Skip to content

Commit c820e04

Browse files
authored
Add support for batch change zone records endpoint (#477)
Add support for batch change zone records endpoint Addresses dnsimple/dnsimple-app#31926 Belongs to dnsimple/dnsimple-business#2263
1 parent 4123ae6 commit c820e04

File tree

5 files changed

+389
-1
lines changed

5 files changed

+389
-1
lines changed

dnsimple/service/zones.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from dnsimple.response import Response
2-
from dnsimple.struct import Zone, ZoneDistribution, ZoneFile, ZoneRecord
2+
from dnsimple.struct import Zone, ZoneDistribution, ZoneFile, ZoneRecord, BatchChangeZoneRecordsResponse
33

44

55
class Zones(object):
@@ -264,3 +264,22 @@ def check_zone_record_distribution(self, account_id, zone, record_id):
264264
"""
265265
response = self.client.get(f'/{account_id}/zones/{zone}/records/{record_id}/distribution')
266266
return Response(response, ZoneDistribution)
267+
268+
def batch_change_records(self, account_id, zone, batch_change):
269+
"""
270+
Batch change zone records in the account.
271+
272+
See https://developer.dnsimple.com/v2/zones/records/#batchChangeZoneRecords
273+
274+
:param account_id: int
275+
The account ID
276+
:param zone: str
277+
The zone name
278+
:param batch_change: dnsimple.struct.BatchChangeZoneRecordsInput
279+
The data to send to batch change zone records
280+
281+
:return: dnsimple.Response
282+
The batch change zone records response
283+
"""
284+
response = self.client.post(f'/{account_id}/zones/{zone}/batch', data=batch_change.to_json())
285+
return Response(response, BatchChangeZoneRecordsResponse)

dnsimple/struct/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,11 @@
3333
from dnsimple.struct.zone_distribution import ZoneDistribution
3434
from dnsimple.struct.zone_file import ZoneFile
3535
from dnsimple.struct.zone_record import ZoneRecord, ZoneRecordInput, ZoneRecordUpdateInput
36+
from dnsimple.struct.batch_change_zone_records import (
37+
BatchChangeZoneRecordsInput,
38+
BatchChangeZoneRecordsCreateInput,
39+
BatchChangeZoneRecordsUpdateInput,
40+
BatchChangeZoneRecordsDeleteInput,
41+
BatchChangeZoneRecordsResponse,
42+
BatchChangeZoneRecordsDeleteResponse
43+
)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import json
2+
from dataclasses import dataclass
3+
4+
import omitempty
5+
6+
from dnsimple.struct import Struct
7+
from dnsimple.struct.zone_record import ZoneRecord, ZoneRecordInput
8+
9+
BatchChangeZoneRecordsCreateInput = ZoneRecordInput
10+
11+
12+
@dataclass
13+
class BatchChangeZoneRecordsUpdateInput(dict):
14+
"""Represents a zone record update input for a batch operation"""
15+
16+
def __init__(self, id, name=None, content=None, ttl=None, priority=None, regions=None):
17+
dict.__init__(self, id=id, name=name, content=content, ttl=ttl, priority=priority, regions=regions)
18+
19+
def to_json(self):
20+
omitted = omitempty(self)
21+
22+
if self['name'] == '':
23+
omitted['name'] = ''
24+
25+
return json.dumps(omitted)
26+
27+
28+
@dataclass
29+
class BatchChangeZoneRecordsDeleteInput(dict):
30+
"""Represents a zone record deletion input for a batch operation"""
31+
32+
def __init__(self, id):
33+
dict.__init__(self, id=id)
34+
35+
36+
@dataclass
37+
class BatchChangeZoneRecordsInput(dict):
38+
"""Represents the data to send to the DNSimple API to make a batch change on the records of a zone
39+
40+
All parameters are optional - you can perform creates only, updates only, deletes only,
41+
or any combination of the three operations.
42+
43+
:param creates: List[BatchChangeZoneRecordsCreateInput] - Records to create (optional)
44+
:param updates: List[BatchChangeZoneRecordsUpdateInput] - Records to update (optional)
45+
:param deletes: List[BatchChangeZoneRecordsDeleteInput] - Records to delete (optional)
46+
"""
47+
48+
def __init__(self, creates=None, updates=None, deletes=None):
49+
data = {}
50+
if creates is not None:
51+
data['creates'] = creates
52+
if updates is not None:
53+
data['updates'] = updates
54+
if deletes is not None:
55+
data['deletes'] = deletes
56+
dict.__init__(self, **data)
57+
58+
def to_json(self):
59+
result = {}
60+
61+
if 'creates' in self and self['creates'] is not None:
62+
result['creates'] = [json.loads(item.to_json()) for item in self['creates']]
63+
64+
if 'updates' in self and self['updates'] is not None:
65+
result['updates'] = [json.loads(item.to_json()) for item in self['updates']]
66+
67+
if 'deletes' in self and self['deletes'] is not None:
68+
result['deletes'] = [omitempty(item) for item in self['deletes']]
69+
70+
return json.dumps(result)
71+
72+
73+
@dataclass
74+
class BatchChangeZoneRecordsDeleteResponse(Struct):
75+
"""Represents a deleted zone record in the batch change response"""
76+
id = None
77+
"""The record ID that was deleted"""
78+
79+
def __init__(self, data):
80+
super().__init__(data)
81+
82+
83+
@dataclass
84+
class BatchChangeZoneRecordsResponse(Struct):
85+
"""Represents the response from batch changing zone records"""
86+
creates = None
87+
"""List of created zone records"""
88+
updates = None
89+
"""List of updated zone records"""
90+
deletes = None
91+
"""List of deleted zone record IDs"""
92+
93+
def __init__(self, data):
94+
super().__init__(data)
95+
if 'creates' in data and data['creates'] is not None:
96+
self.creates = [ZoneRecord(item) for item in data['creates']]
97+
if 'updates' in data and data['updates'] is not None:
98+
self.updates = [ZoneRecord(item) for item in data['updates']]
99+
if 'deletes' in data and data['deletes'] is not None:
100+
self.deletes = [BatchChangeZoneRecordsDeleteResponse(item) for item in data['deletes']]

tests/service/zones_test.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44

55
from dnsimple import DNSimpleException
66
from dnsimple.struct.zone import Zone
7+
from dnsimple.struct.batch_change_zone_records import (
8+
BatchChangeZoneRecordsInput,
9+
BatchChangeZoneRecordsUpdateInput,
10+
BatchChangeZoneRecordsDeleteInput,
11+
BatchChangeZoneRecordsResponse
12+
)
13+
from dnsimple.struct.zone_record import ZoneRecordInput
14+
from dnsimple.struct.zone_record import ZoneRecord
715
from tests.helpers import DNSimpleTest, DNSimpleMockResponse
816

917

@@ -126,6 +134,105 @@ def test_check_zone_distribution_failure(self):
126134
self.assertEqual('Could not query zone, connection time out', dnse.message)
127135
self.assertIsInstance(dnse, DNSimpleException)
128136

137+
@responses.activate
138+
def test_batch_change_records_success(self):
139+
responses.add(DNSimpleMockResponse(method=responses.POST,
140+
path='/1010/zones/example.com/batch',
141+
fixture_name='batchChangeZoneRecords/success'))
142+
143+
batch_change = BatchChangeZoneRecordsInput(
144+
creates=[
145+
ZoneRecordInput('ab', 'A', '3.2.3.4'),
146+
ZoneRecordInput('ab', 'A', '4.2.3.4')
147+
],
148+
updates=[
149+
BatchChangeZoneRecordsUpdateInput(67622534, content='3.2.3.40'),
150+
BatchChangeZoneRecordsUpdateInput(67622537, content='5.2.3.40')
151+
],
152+
deletes=[
153+
BatchChangeZoneRecordsDeleteInput(67622509),
154+
BatchChangeZoneRecordsDeleteInput(67622527)
155+
]
156+
)
157+
158+
response = self.zones.batch_change_records(1010, 'example.com', batch_change)
159+
result = response.data
160+
161+
self.assertIsInstance(result, BatchChangeZoneRecordsResponse)
162+
163+
self.assertEqual(2, len(result.creates))
164+
self.assertIsInstance(result.creates[0], ZoneRecord)
165+
self.assertEqual(67623409, result.creates[0].id)
166+
self.assertEqual('ab', result.creates[0].name)
167+
self.assertEqual('3.2.3.4', result.creates[0].content)
168+
self.assertEqual('A', result.creates[0].type)
169+
170+
self.assertEqual(2, len(result.updates))
171+
self.assertIsInstance(result.updates[0], ZoneRecord)
172+
self.assertEqual(67622534, result.updates[0].id)
173+
self.assertEqual('3.2.3.40', result.updates[0].content)
174+
175+
self.assertEqual(2, len(result.deletes))
176+
self.assertEqual(67622509, result.deletes[0].id)
177+
self.assertEqual(67622527, result.deletes[1].id)
178+
179+
@responses.activate
180+
def test_batch_change_records_create_validation_failed(self):
181+
responses.add(DNSimpleMockResponse(method=responses.POST,
182+
path='/1010/zones/example.com/batch',
183+
fixture_name='batchChangeZoneRecords/error_400_create_validation_failed'))
184+
185+
batch_change = BatchChangeZoneRecordsInput(
186+
creates=[
187+
ZoneRecordInput('test', 'SPF', 'v=spf1 -all')
188+
]
189+
)
190+
191+
try:
192+
self.zones.batch_change_records(1010, 'example.com', batch_change)
193+
except DNSimpleException as dnse:
194+
self.assertEqual('Validation failed', dnse.message)
195+
self.assertIsInstance(dnse, DNSimpleException)
196+
self.assertEqual('The SPF record type has been discontinued', dnse.attribute_errors['creates'][0]['message'])
197+
198+
@responses.activate
199+
def test_batch_change_records_update_validation_failed(self):
200+
responses.add(DNSimpleMockResponse(method=responses.POST,
201+
path='/1010/zones/example.com/batch',
202+
fixture_name='batchChangeZoneRecords/error_400_update_validation_failed'))
203+
204+
batch_change = BatchChangeZoneRecordsInput(
205+
updates=[
206+
BatchChangeZoneRecordsUpdateInput(99999999, content='1.2.3.4')
207+
]
208+
)
209+
210+
try:
211+
self.zones.batch_change_records(1010, 'example.com', batch_change)
212+
except DNSimpleException as dnse:
213+
self.assertEqual('Validation failed', dnse.message)
214+
self.assertIsInstance(dnse, DNSimpleException)
215+
self.assertEqual('Record not found ID=99999999', dnse.attribute_errors['updates'][0]['message'])
216+
217+
@responses.activate
218+
def test_batch_change_records_delete_validation_failed(self):
219+
responses.add(DNSimpleMockResponse(method=responses.POST,
220+
path='/1010/zones/example.com/batch',
221+
fixture_name='batchChangeZoneRecords/error_400_delete_validation_failed'))
222+
223+
batch_change = BatchChangeZoneRecordsInput(
224+
deletes=[
225+
BatchChangeZoneRecordsDeleteInput(67622509)
226+
]
227+
)
228+
229+
try:
230+
self.zones.batch_change_records(1010, 'example.com', batch_change)
231+
except DNSimpleException as dnse:
232+
self.assertEqual('Validation failed', dnse.message)
233+
self.assertIsInstance(dnse, DNSimpleException)
234+
self.assertEqual('Record not found ID=67622509', dnse.attribute_errors['deletes'][0]['message'])
235+
129236

130237
if __name__ == '__main__':
131238
unittest.main()

0 commit comments

Comments
 (0)