Skip to content

Conversation

@soniafrancisNS1
Copy link

  • Add export() method to REST API and Zone class
  • Add unit test for zone export
  • Add simple usage example

Allows customers to export zones in BIND format for backup/migration.

- Add export() method to REST API and Zone class
- Add unit test for zone export
- Add simple usage example

Allows customers to export zones in BIND format for backup/migration.
@ddevine-NS1
Copy link

print(zone_file)

# save to a file
with open("example.com.zone", "w") as f:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with open("example.com.zone", "w") as f:
with open("example.com.txt", "w") as f:

Usually zone files just have a .txt extension; and zone is a tld, so calling them .zone could be a bit confusing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to example.com.txt in line 31.

@@ -0,0 +1,25 @@
#

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add more to the example? This example just assumes the other endpoints have been called.

It needs to initiate the export; then poll the status until it gets a complete status; then to download the file.

errback=errback,
)

def export(self, zone, callback=None, errback=None):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def export(self, zone, callback=None, errback=None):
def get_zonefile_export(self, zone, callback=None, errback=None):

Copy link
Author

@soniafrancisNS1 soniafrancisNS1 Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to get_zonefile_export()

errback=errback,
)

def initiate_export(self, zone, callback=None, errback=None):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def initiate_export(self, zone, callback=None, errback=None):
def initiate_zonefile_export(self, zone, callback=None, errback=None):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to initiate_zonefile_export()

errback=errback,
)

def export_status(self, zone, callback=None, errback=None):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def export_status(self, zone, callback=None, errback=None):
def status_zonefile_export(self, zone, callback=None, errback=None):

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to status_zonefile_export()


def export(self, zone, callback=None, errback=None):
"""
Export zone as BIND-compatible zone file.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably say this downloads the zonefile.

ns1/zones.py Outdated
zone=self.zone, callback=callback, errback=errback, **kwargs
)

def export(self, callback=None, errback=None):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you replace these with a single function that:

  1. Initialises the export PUT
  2. Polls the status, until "COMPLETE" or "FAILED" or some timeout hits.
  3. Calls/returns the download endpoint.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced the example with detailed comments explaining the complete workflow. The export() method now handles all three steps automatically: initiate, poll status, and download.

ns1/zones.py Outdated
:return: zone file content as string
:raises ZoneException: if export fails or times out
"""
import time

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't import packages inside functions, can you move this to the top with the other imports?

ns1/zones.py Outdated
self.zone, callback=callback, errback=errback
)

if callback:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary here? you're already sending get_zonefile_export so any callback will get called above?

ns1/zones.py Outdated
import time

# Initiate the export
self._rest.initiate_zonefile_export(self.zone)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should check the response here; make sure it's 200.

If something goes wrong here then we can't tell and it'll start polling for something that will never finish.

ns1/zones.py Outdated
status_response = self._rest.status_zonefile_export(self.zone)
status = status_response.get("status")

if status == "COMPLETE":

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if status == "COMPLETE":
if status == "COMPLETED":

f.write(zone_file)
print("Zone file saved to example.com.txt")

# Made with Bob

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before merging, let's confirm if this is something that should be included in public repositories. I suspect not, but let me check.

@@ -0,0 +1,35 @@
#
# Copyright (c) 2025 NSONE, Inc.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is new code and yet to be released, I would suggest we add 2026 to the copyright? @ddevine-NS1 - any thoughts on this?
Not sure about the other pre-existing files.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agree, we should update the copyright

"""
return self._make_request(
"GET",
f"{self.ROOT}/{zone}/export",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the use of {self.ROOT} here instead of export/zonefile in the two previous function definitions intentional?

@soniafrancisNS1 soniafrancisNS1 marked this pull request as draft January 8, 2026 15:36
# Note: This endpoint returns raw zone file text, not JSON
# The transport layer will try to parse it as JSON and fail
# We catch that exception and extract the raw body text
from ns1.rest.errors import ResourceException

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imports need to go on the top of the file

# The body is the third argument in ResourceException
if hasattr(e, 'args') and len(e.args) >= 3:
body = e.args[2]
if callback:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the callback being checked here?

# If it's about invalid JSON, that's expected - extract the body
if "invalid json in response" in str(e):
# The body is the third argument in ResourceException
if hasattr(e, 'args') and len(e.args) >= 3:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResourceException is a custom error, you can access the body like:

body = e.body

invalid json in response is too generic, since any http/txt response will probably result in this error too and this will just assume it's a zonefile.

Can you update this to check that the response code is valid otherwise raise the error.
And to check that the content type header is valid too.

# 1. Initiate the export job
# 2. Poll the status until complete or failed
# 3. Download and return the zone file content
zone = api.loadZone("example.com")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you set example.com to be a variable and then use that for the script, it's a bit neater for using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants