Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions examples/zone-export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#
Copy link
Copy Markdown

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.

# Copyright (c) 2025 NSONE, Inc.
Copy link
Copy Markdown

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.

Copy link
Copy Markdown

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

#
# License under The MIT License (MIT). See LICENSE in project root.
#

from ns1 import NS1

# NS1 will use config in ~/.nsone by default
api = NS1()

# to specify an apikey here instead, use:
# api = NS1(apiKey='<<CLEARTEXT API KEY>>')

# to load an alternate configuration file:
# api = NS1(configFile='/etc/ns1/api.json')

# export a zone to BIND format
zone = api.loadZone("example.com")
Copy link
Copy Markdown

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Made the output filename dynamic using f"{zone_name}.txt"

zone_file = zone.export()
print(zone_file)

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

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
Copy Markdown
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.

f.write(zone_file)
43 changes: 43 additions & 0 deletions ns1/rest/zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,49 @@ def delete_version(self, zone, version_id, callback=None, errback=None):
errback=errback,
)

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

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
Copy Markdown
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()

"""
Export zone as BIND-compatible zone file.
Copy link
Copy Markdown

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.


:param str zone: zone name
:return: zone file content as string
"""
return self._make_request(
"GET",
f"{self.ROOT}/{zone}/export",
callback=callback,
errback=errback,
)

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

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
Copy Markdown
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()

"""
Initiate zone export job.

:param str zone: zone name
:return: export status response
"""
return self._make_request(
"PUT",
f"export/zonefile/{zone}",
body={},
callback=callback,
errback=errback,
)

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

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
Copy Markdown
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()

"""
Check zone export status.

:param str zone: zone name
:return: export status response
"""
return self._make_request(
"GET",
f"export/zonefile/{zone}/status",
callback=callback,
errback=errback,
)


# successive pages just extend the list of zones
def zone_list_pagination(curr_json, next_json):
Expand Down
34 changes: 34 additions & 0 deletions ns1/zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,37 @@ def usage(self, callback=None, errback=None, **kwargs):
return stats.usage(
zone=self.zone, callback=callback, errback=errback, **kwargs
)

def export(self, callback=None, errback=None):
Copy link
Copy Markdown

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
Copy Markdown
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.

"""
Export zone as a BIND-compatible zone file.

:param callback: optional callback
:param errback: optional error callback
:return: zone file content as string
"""
return self._rest.export(self.zone, callback=callback, errback=errback)

def initiate_export(self, callback=None, errback=None):
"""
Initiate zone export job.

:param callback: optional callback
:param errback: optional error callback
:return: export status response
"""
return self._rest.initiate_export(
self.zone, callback=callback, errback=errback
)

def export_status(self, callback=None, errback=None):
"""
Check zone export status.

:param callback: optional callback
:param errback: optional error callback
:return: export status response
"""
return self._rest.export_status(
self.zone, callback=callback, errback=errback
)
46 changes: 46 additions & 0 deletions tests/unit/test_zone.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,49 @@ def test_rest_zone_buildbody(zones_config):
"tags": {"foo": "bar", "hai": "bai"},
}
assert z._buildBody(zone, **kwargs) == body


@pytest.mark.parametrize(
"zone, url", [("test.zone", "zones/test.zone/export")]
)
def test_rest_zone_export(zones_config, zone, url):
z = ns1.rest.zones.Zones(zones_config)
z._make_request = mock.MagicMock()
z.export(zone)
z._make_request.assert_called_once_with(
"GET",
url,
callback=None,
errback=None,
)


@pytest.mark.parametrize(
"zone, url", [("test.zone", "export/zonefile/test.zone")]
)
def test_rest_zone_initiate_export(zones_config, zone, url):
z = ns1.rest.zones.Zones(zones_config)
z._make_request = mock.MagicMock()
z.initiate_export(zone)
z._make_request.assert_called_once_with(
"PUT",
url,
body={},
callback=None,
errback=None,
)


@pytest.mark.parametrize(
"zone, url", [("test.zone", "export/zonefile/test.zone/status")]
)
def test_rest_zone_export_status(zones_config, zone, url):
z = ns1.rest.zones.Zones(zones_config)
z._make_request = mock.MagicMock()
z.export_status(zone)
z._make_request.assert_called_once_with(
"GET",
url,
callback=None,
errback=None,
)