dnscale 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. dnscale/__init__.py +27 -0
  2. dnscale/client.py +125 -0
  3. dnscale/errors.py +118 -0
  4. dnscale/pagination.py +20 -0
  5. dnscale/py.typed +1 -0
  6. dnscale/records.py +295 -0
  7. dnscale/transport.py +129 -0
  8. dnscale/zones.py +156 -0
  9. dnscale-1.0.0.dist-info/METADATA +200 -0
  10. dnscale-1.0.0.dist-info/RECORD +286 -0
  11. dnscale-1.0.0.dist-info/WHEEL +5 -0
  12. dnscale-1.0.0.dist-info/licenses/LICENSE +21 -0
  13. dnscale-1.0.0.dist-info/top_level.txt +2 -0
  14. dnscale_api/__init__.py +8 -0
  15. dnscale_api/api/__init__.py +1 -0
  16. dnscale_api/api/activity/__init__.py +1 -0
  17. dnscale_api/api/activity/list_activity.py +255 -0
  18. dnscale_api/api/alerts/__init__.py +1 -0
  19. dnscale_api/api/alerts/create_alert.py +157 -0
  20. dnscale_api/api/alerts/delete_alert.py +147 -0
  21. dnscale_api/api/alerts/get_alert.py +151 -0
  22. dnscale_api/api/alerts/list_alerts.py +208 -0
  23. dnscale_api/api/alerts/update_alert.py +173 -0
  24. dnscale_api/api/api_keys/__init__.py +1 -0
  25. dnscale_api/api/api_keys/create_api_key.py +196 -0
  26. dnscale_api/api/api_keys/delete_api_key.py +162 -0
  27. dnscale_api/api/api_keys/get_api_key.py +168 -0
  28. dnscale_api/api/api_keys/list_api_keys.py +154 -0
  29. dnscale_api/api/billing/__init__.py +1 -0
  30. dnscale_api/api/billing/get_billing_history.py +173 -0
  31. dnscale_api/api/billing/get_billing_summary.py +158 -0
  32. dnscale_api/api/billing/list_invoices.py +173 -0
  33. dnscale_api/api/dns_control/__init__.py +1 -0
  34. dnscale_api/api/dns_control/create_dns_control_connection.py +159 -0
  35. dnscale_api/api/dns_control/create_dns_control_connection_zone_snapshot.py +191 -0
  36. dnscale_api/api/dns_control/create_dns_control_inventory_snapshot.py +154 -0
  37. dnscale_api/api/dns_control/diff_dns_control_snapshots.py +182 -0
  38. dnscale_api/api/dns_control/get_dns_control_snapshot.py +168 -0
  39. dnscale_api/api/dns_control/list_dns_control_connection_zones.py +154 -0
  40. dnscale_api/api/dns_control/list_dns_control_connections.py +126 -0
  41. dnscale_api/api/dns_control/list_dns_control_inventory.py +126 -0
  42. dnscale_api/api/dns_control/list_dns_control_providers.py +126 -0
  43. dnscale_api/api/dns_control/list_dns_control_snapshots.py +190 -0
  44. dnscale_api/api/dns_control/save_dns_control_zone_inventory.py +191 -0
  45. dnscale_api/api/dnssec/__init__.py +1 -0
  46. dnscale_api/api/dnssec/create_dnssec_cryptokey.py +176 -0
  47. dnscale_api/api/dnssec/delete_dnssec_cryptokey.py +162 -0
  48. dnscale_api/api/dnssec/get_dnssec_cryptokey.py +168 -0
  49. dnscale_api/api/dnssec/get_dnssec_status.py +154 -0
  50. dnscale_api/api/dnssec/list_dnssec_cryptokeys.py +154 -0
  51. dnscale_api/api/dnssec/list_dnssecds_records.py +154 -0
  52. dnscale_api/api/dnssec/set_dnssec_status.py +170 -0
  53. dnscale_api/api/dnssec/update_dnssec_cryptokey.py +184 -0
  54. dnscale_api/api/health/__init__.py +1 -0
  55. dnscale_api/api/health/get_health.py +123 -0
  56. dnscale_api/api/invitations/__init__.py +1 -0
  57. dnscale_api/api/invitations/create_invitation.py +159 -0
  58. dnscale_api/api/invitations/delete_invitation.py +148 -0
  59. dnscale_api/api/invitations/get_invitation.py +154 -0
  60. dnscale_api/api/invitations/list_invitations.py +173 -0
  61. dnscale_api/api/records/__init__.py +1 -0
  62. dnscale_api/api/records/create_record.py +176 -0
  63. dnscale_api/api/records/delete_record.py +162 -0
  64. dnscale_api/api/records/delete_records_by_name_and_type.py +206 -0
  65. dnscale_api/api/records/get_record.py +168 -0
  66. dnscale_api/api/records/list_records.py +190 -0
  67. dnscale_api/api/records/update_record.py +198 -0
  68. dnscale_api/api/records/update_record_by_name_and_type.py +233 -0
  69. dnscale_api/api/usage/__init__.py +1 -0
  70. dnscale_api/api/usage/get_current_usage.py +126 -0
  71. dnscale_api/api/usage/get_daily_usage.py +199 -0
  72. dnscale_api/api/usage/get_usage_summary.py +158 -0
  73. dnscale_api/api/usage/get_zone_usage.py +197 -0
  74. dnscale_api/api/usage/list_top_zones_by_usage.py +195 -0
  75. dnscale_api/api/users/__init__.py +1 -0
  76. dnscale_api/api/users/create_user.py +157 -0
  77. dnscale_api/api/users/delete_user.py +148 -0
  78. dnscale_api/api/users/get_user.py +152 -0
  79. dnscale_api/api/users/list_users.py +171 -0
  80. dnscale_api/api/users/update_user.py +178 -0
  81. dnscale_api/api/zones/__init__.py +1 -0
  82. dnscale_api/api/zones/create_zone.py +157 -0
  83. dnscale_api/api/zones/delete_zone.py +148 -0
  84. dnscale_api/api/zones/get_zone.py +152 -0
  85. dnscale_api/api/zones/list_zones.py +171 -0
  86. dnscale_api/api/zones/update_zone.py +178 -0
  87. dnscale_api/client.py +268 -0
  88. dnscale_api/errors.py +16 -0
  89. dnscale_api/models/__init__.py +399 -0
  90. dnscale_api/models/activity_data.py +66 -0
  91. dnscale_api/models/activity_item.py +236 -0
  92. dnscale_api/models/activity_item_new_values_type_0.py +46 -0
  93. dnscale_api/models/activity_item_old_values_type_0.py +46 -0
  94. dnscale_api/models/activity_response.py +58 -0
  95. dnscale_api/models/activity_response_status.py +8 -0
  96. dnscale_api/models/aggregated_daily_stat.py +92 -0
  97. dnscale_api/models/aggregated_usage_response.py +100 -0
  98. dnscale_api/models/aggregated_usage_response_envelope.py +58 -0
  99. dnscale_api/models/aggregated_usage_response_envelope_status.py +8 -0
  100. dnscale_api/models/alert.py +210 -0
  101. dnscale_api/models/alert_actions.py +46 -0
  102. dnscale_api/models/alert_channel.py +125 -0
  103. dnscale_api/models/alert_channel_type.py +11 -0
  104. dnscale_api/models/alert_data.py +48 -0
  105. dnscale_api/models/alert_response.py +58 -0
  106. dnscale_api/models/alert_response_status.py +8 -0
  107. dnscale_api/models/alert_scope_type.py +9 -0
  108. dnscale_api/models/alert_severity.py +10 -0
  109. dnscale_api/models/alert_status.py +9 -0
  110. dnscale_api/models/alert_threshold_config.py +114 -0
  111. dnscale_api/models/alerts_data.py +56 -0
  112. dnscale_api/models/alerts_response.py +58 -0
  113. dnscale_api/models/alerts_response_status.py +8 -0
  114. dnscale_api/models/api_key.py +172 -0
  115. dnscale_api/models/api_key_data.py +48 -0
  116. dnscale_api/models/api_key_response.py +58 -0
  117. dnscale_api/models/api_key_response_status.py +8 -0
  118. dnscale_api/models/api_key_zone_scope.py +66 -0
  119. dnscale_api/models/api_key_zone_scope_record_access.py +9 -0
  120. dnscale_api/models/api_keys_data.py +56 -0
  121. dnscale_api/models/api_keys_response.py +58 -0
  122. dnscale_api/models/api_keys_response_status.py +8 -0
  123. dnscale_api/models/billing_history.py +64 -0
  124. dnscale_api/models/billing_history_response_envelope.py +58 -0
  125. dnscale_api/models/billing_history_response_envelope_status.py +8 -0
  126. dnscale_api/models/billing_summary.py +127 -0
  127. dnscale_api/models/billing_summary_response_envelope.py +58 -0
  128. dnscale_api/models/billing_summary_response_envelope_status.py +8 -0
  129. dnscale_api/models/create_alert_request.py +162 -0
  130. dnscale_api/models/create_alert_request_actions.py +46 -0
  131. dnscale_api/models/create_api_key_request.py +140 -0
  132. dnscale_api/models/create_cryptokey_request.py +135 -0
  133. dnscale_api/models/create_cryptokey_request_keytype.py +10 -0
  134. dnscale_api/models/create_dns_control_connection_request.py +144 -0
  135. dnscale_api/models/create_dns_control_connection_request_mode.py +8 -0
  136. dnscale_api/models/create_dns_control_connection_request_provider.py +9 -0
  137. dnscale_api/models/create_invitation_request.py +42 -0
  138. dnscale_api/models/create_record_request.py +119 -0
  139. dnscale_api/models/create_user_request.py +79 -0
  140. dnscale_api/models/create_zone_request.py +95 -0
  141. dnscale_api/models/cryptokey.py +127 -0
  142. dnscale_api/models/cryptokey_data.py +48 -0
  143. dnscale_api/models/cryptokey_keytype.py +10 -0
  144. dnscale_api/models/cryptokey_response.py +58 -0
  145. dnscale_api/models/cryptokey_response_status.py +8 -0
  146. dnscale_api/models/cryptokeys_data.py +56 -0
  147. dnscale_api/models/cryptokeys_response.py +58 -0
  148. dnscale_api/models/cryptokeys_response_status.py +8 -0
  149. dnscale_api/models/customer_usage_response.py +58 -0
  150. dnscale_api/models/customer_usage_response_status.py +8 -0
  151. dnscale_api/models/customer_usage_summary.py +191 -0
  152. dnscale_api/models/customer_usage_summary_billing_type.py +9 -0
  153. dnscale_api/models/dns_control_auth_field.py +87 -0
  154. dnscale_api/models/dns_control_auth_field_type.py +10 -0
  155. dnscale_api/models/dns_control_connection.py +209 -0
  156. dnscale_api/models/dns_control_connection_data.py +48 -0
  157. dnscale_api/models/dns_control_connection_mode.py +8 -0
  158. dnscale_api/models/dns_control_connection_response.py +58 -0
  159. dnscale_api/models/dns_control_connection_response_status.py +8 -0
  160. dnscale_api/models/dns_control_connection_snapshot_data.py +58 -0
  161. dnscale_api/models/dns_control_connection_snapshot_response.py +60 -0
  162. dnscale_api/models/dns_control_connection_snapshot_response_status.py +8 -0
  163. dnscale_api/models/dns_control_connection_status.py +11 -0
  164. dnscale_api/models/dns_control_connections_data.py +56 -0
  165. dnscale_api/models/dns_control_connections_response.py +58 -0
  166. dnscale_api/models/dns_control_connections_response_status.py +8 -0
  167. dnscale_api/models/dns_control_inventory.py +149 -0
  168. dnscale_api/models/dns_control_inventory_data.py +48 -0
  169. dnscale_api/models/dns_control_inventory_delegation.py +46 -0
  170. dnscale_api/models/dns_control_inventory_dnssec.py +46 -0
  171. dnscale_api/models/dns_control_inventory_list_data.py +56 -0
  172. dnscale_api/models/dns_control_inventory_list_response.py +60 -0
  173. dnscale_api/models/dns_control_inventory_list_response_status.py +8 -0
  174. dnscale_api/models/dns_control_inventory_response.py +58 -0
  175. dnscale_api/models/dns_control_inventory_response_status.py +8 -0
  176. dnscale_api/models/dns_control_provider.py +121 -0
  177. dnscale_api/models/dns_control_provider_auth_type.py +11 -0
  178. dnscale_api/models/dns_control_provider_capabilities.py +135 -0
  179. dnscale_api/models/dns_control_provider_type.py +9 -0
  180. dnscale_api/models/dns_control_provider_zone.py +121 -0
  181. dnscale_api/models/dns_control_provider_zone_dnssec.py +46 -0
  182. dnscale_api/models/dns_control_provider_zone_metadata.py +46 -0
  183. dnscale_api/models/dns_control_provider_zone_request.py +42 -0
  184. dnscale_api/models/dns_control_provider_zones_data.py +56 -0
  185. dnscale_api/models/dns_control_provider_zones_response.py +60 -0
  186. dnscale_api/models/dns_control_provider_zones_response_status.py +8 -0
  187. dnscale_api/models/dns_control_providers_data.py +56 -0
  188. dnscale_api/models/dns_control_providers_response.py +58 -0
  189. dnscale_api/models/dns_control_providers_response_status.py +8 -0
  190. dnscale_api/models/dns_control_record_set.py +191 -0
  191. dnscale_api/models/dns_control_record_set_provider_metadata.py +46 -0
  192. dnscale_api/models/dns_control_record_set_raw_refs_item.py +46 -0
  193. dnscale_api/models/dns_control_record_set_values_item.py +46 -0
  194. dnscale_api/models/dns_control_snapshot.py +141 -0
  195. dnscale_api/models/dns_control_snapshot_canonical_zone.py +46 -0
  196. dnscale_api/models/dns_control_snapshot_data.py +48 -0
  197. dnscale_api/models/dns_control_snapshot_diff.py +83 -0
  198. dnscale_api/models/dns_control_snapshot_diff_change.py +133 -0
  199. dnscale_api/models/dns_control_snapshot_diff_change_after.py +46 -0
  200. dnscale_api/models/dns_control_snapshot_diff_change_before.py +46 -0
  201. dnscale_api/models/dns_control_snapshot_diff_change_severity.py +10 -0
  202. dnscale_api/models/dns_control_snapshot_diff_change_type.py +12 -0
  203. dnscale_api/models/dns_control_snapshot_diff_data.py +48 -0
  204. dnscale_api/models/dns_control_snapshot_diff_response.py +58 -0
  205. dnscale_api/models/dns_control_snapshot_diff_response_status.py +8 -0
  206. dnscale_api/models/dns_control_snapshot_diff_summary.py +82 -0
  207. dnscale_api/models/dns_control_snapshot_response.py +58 -0
  208. dnscale_api/models/dns_control_snapshot_response_status.py +8 -0
  209. dnscale_api/models/dns_control_snapshot_source_kind.py +8 -0
  210. dnscale_api/models/dns_control_snapshots_data.py +66 -0
  211. dnscale_api/models/dns_control_snapshots_response.py +58 -0
  212. dnscale_api/models/dns_control_snapshots_response_status.py +8 -0
  213. dnscale_api/models/dnssec_status_data.py +42 -0
  214. dnscale_api/models/dnssec_status_response.py +58 -0
  215. dnscale_api/models/dnssec_status_response_status.py +8 -0
  216. dnscale_api/models/ds_records_data.py +42 -0
  217. dnscale_api/models/ds_records_response.py +58 -0
  218. dnscale_api/models/ds_records_response_status.py +8 -0
  219. dnscale_api/models/error_detail.py +79 -0
  220. dnscale_api/models/error_detail_details_item.py +46 -0
  221. dnscale_api/models/error_response.py +58 -0
  222. dnscale_api/models/error_response_status.py +8 -0
  223. dnscale_api/models/health.py +62 -0
  224. dnscale_api/models/health_data.py +48 -0
  225. dnscale_api/models/health_response.py +58 -0
  226. dnscale_api/models/health_response_status.py +8 -0
  227. dnscale_api/models/health_status.py +8 -0
  228. dnscale_api/models/invitation.py +113 -0
  229. dnscale_api/models/invitation_data.py +48 -0
  230. dnscale_api/models/invitation_response.py +58 -0
  231. dnscale_api/models/invitation_response_status.py +8 -0
  232. dnscale_api/models/invitation_status.py +11 -0
  233. dnscale_api/models/invitations_data.py +56 -0
  234. dnscale_api/models/invitations_response.py +58 -0
  235. dnscale_api/models/invitations_response_status.py +8 -0
  236. dnscale_api/models/invoice.py +281 -0
  237. dnscale_api/models/invoices_data.py +64 -0
  238. dnscale_api/models/invoices_response.py +58 -0
  239. dnscale_api/models/invoices_response_status.py +8 -0
  240. dnscale_api/models/offset_meta.py +50 -0
  241. dnscale_api/models/pagination.py +74 -0
  242. dnscale_api/models/provider_credentials.py +79 -0
  243. dnscale_api/models/record.py +125 -0
  244. dnscale_api/models/record_data.py +48 -0
  245. dnscale_api/models/record_response.py +58 -0
  246. dnscale_api/models/record_response_status.py +8 -0
  247. dnscale_api/models/record_type.py +22 -0
  248. dnscale_api/models/records_data.py +66 -0
  249. dnscale_api/models/records_response.py +58 -0
  250. dnscale_api/models/records_response_status.py +8 -0
  251. dnscale_api/models/set_dnssec_request.py +42 -0
  252. dnscale_api/models/top_zones_data.py +56 -0
  253. dnscale_api/models/top_zones_response.py +58 -0
  254. dnscale_api/models/top_zones_response_status.py +8 -0
  255. dnscale_api/models/update_alert_request.py +176 -0
  256. dnscale_api/models/update_alert_request_actions.py +46 -0
  257. dnscale_api/models/update_cryptokey_request.py +51 -0
  258. dnscale_api/models/update_record_by_name_request.py +102 -0
  259. dnscale_api/models/update_user_request.py +80 -0
  260. dnscale_api/models/update_zone_request.py +96 -0
  261. dnscale_api/models/usage_map.py +46 -0
  262. dnscale_api/models/user.py +124 -0
  263. dnscale_api/models/user_data.py +48 -0
  264. dnscale_api/models/user_response.py +58 -0
  265. dnscale_api/models/user_response_status.py +8 -0
  266. dnscale_api/models/user_role.py +9 -0
  267. dnscale_api/models/users_data.py +56 -0
  268. dnscale_api/models/users_response.py +58 -0
  269. dnscale_api/models/users_response_status.py +8 -0
  270. dnscale_api/models/zone.py +128 -0
  271. dnscale_api/models/zone_data.py +48 -0
  272. dnscale_api/models/zone_region.py +10 -0
  273. dnscale_api/models/zone_response.py +58 -0
  274. dnscale_api/models/zone_response_status.py +8 -0
  275. dnscale_api/models/zone_settings_brief.py +42 -0
  276. dnscale_api/models/zone_status.py +11 -0
  277. dnscale_api/models/zone_type.py +9 -0
  278. dnscale_api/models/zone_usage_daily.py +163 -0
  279. dnscale_api/models/zone_usage_response.py +58 -0
  280. dnscale_api/models/zone_usage_response_status.py +8 -0
  281. dnscale_api/models/zone_usage_summary.py +128 -0
  282. dnscale_api/models/zones_data.py +66 -0
  283. dnscale_api/models/zones_response.py +102 -0
  284. dnscale_api/models/zones_response_status.py +8 -0
  285. dnscale_api/py.typed +1 -0
  286. dnscale_api/types.py +54 -0
dnscale/__init__.py ADDED
@@ -0,0 +1,27 @@
1
+ """Ergonomic entry point for the DNScale Python SDK."""
2
+
3
+ from dnscale_api.models.record import Record
4
+ from dnscale_api.models.record_type import RecordType
5
+ from dnscale_api.models.zone import Zone
6
+ from dnscale_api.models.zone_region import ZoneRegion
7
+ from dnscale_api.models.zone_status import ZoneStatus
8
+ from dnscale_api.models.zone_type import ZoneType
9
+
10
+ from .client import DEFAULT_BASE_URL, DNScale
11
+ from .errors import APIError, ProtocolError, RateLimitError
12
+
13
+ __all__ = (
14
+ "APIError",
15
+ "DEFAULT_BASE_URL",
16
+ "DNScale",
17
+ "ProtocolError",
18
+ "RateLimitError",
19
+ "Record",
20
+ "RecordType",
21
+ "Zone",
22
+ "ZoneRegion",
23
+ "ZoneStatus",
24
+ "ZoneType",
25
+ )
26
+
27
+ __version__ = "1.0.0"
dnscale/client.py ADDED
@@ -0,0 +1,125 @@
1
+ """Configured DNScale API client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import ssl
7
+ from collections.abc import Mapping
8
+
9
+ import httpx
10
+
11
+ from dnscale_api import AuthenticatedClient
12
+
13
+ from .errors import acheck_error_response, check_error_response
14
+ from .records import Records
15
+ from .transport import AsyncRetryTransport, RetryTransport
16
+ from .zones import Zones
17
+
18
+ DEFAULT_BASE_URL = "https://api.dnscale.eu/v1"
19
+
20
+
21
+ class DNScale:
22
+ """DNScale client with typed resources and access to every generated endpoint.
23
+
24
+ The API key is read from ``DNSCALE_API_KEY`` when it is not passed explicitly.
25
+ Only safe read requests are retried after transient HTTP or transport failures.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ api_key: str | None = None,
31
+ *,
32
+ base_url: str = DEFAULT_BASE_URL,
33
+ timeout: float | httpx.Timeout = 10.0,
34
+ max_retries: int = 2,
35
+ retry_backoff: float = 0.25,
36
+ verify_ssl: str | bool | ssl.SSLContext = True,
37
+ follow_redirects: bool = False,
38
+ headers: Mapping[str, str] | None = None,
39
+ transport: httpx.BaseTransport | None = None,
40
+ async_transport: httpx.AsyncBaseTransport | None = None,
41
+ ) -> None:
42
+ token = api_key or os.getenv("DNSCALE_API_KEY")
43
+ if not token:
44
+ raise ValueError("A DNScale API key is required; pass api_key or set DNSCALE_API_KEY")
45
+ if max_retries < 0:
46
+ raise ValueError("max_retries must be zero or greater")
47
+ if retry_backoff < 0:
48
+ raise ValueError("retry_backoff must be zero or greater")
49
+
50
+ normalized_base_url = base_url.rstrip("/")
51
+ request_headers = {
52
+ "Accept": "application/json",
53
+ "User-Agent": "dnscale-python/1.0.0",
54
+ **dict(headers or {}),
55
+ "Authorization": f"Bearer {token}",
56
+ }
57
+
58
+ sync_client = httpx.Client(
59
+ event_hooks={"response": [check_error_response]},
60
+ base_url=normalized_base_url,
61
+ headers=request_headers,
62
+ timeout=timeout,
63
+ verify=verify_ssl,
64
+ follow_redirects=follow_redirects,
65
+ transport=RetryTransport(
66
+ max_retries=max_retries,
67
+ backoff=retry_backoff,
68
+ verify_ssl=verify_ssl,
69
+ transport=transport,
70
+ ),
71
+ )
72
+ async_client = httpx.AsyncClient(
73
+ event_hooks={"response": [acheck_error_response]},
74
+ base_url=normalized_base_url,
75
+ headers=request_headers,
76
+ timeout=timeout,
77
+ verify=verify_ssl,
78
+ follow_redirects=follow_redirects,
79
+ transport=AsyncRetryTransport(
80
+ max_retries=max_retries,
81
+ backoff=retry_backoff,
82
+ verify_ssl=verify_ssl,
83
+ transport=async_transport,
84
+ ),
85
+ )
86
+
87
+ self.client = AuthenticatedClient(
88
+ base_url=normalized_base_url,
89
+ token=token,
90
+ headers=dict(request_headers),
91
+ timeout=httpx.Timeout(timeout) if isinstance(timeout, float | int) else timeout,
92
+ verify_ssl=verify_ssl,
93
+ follow_redirects=follow_redirects,
94
+ raise_on_unexpected_status=True,
95
+ )
96
+ self.client.set_httpx_client(sync_client)
97
+ self.client.set_async_httpx_client(async_client)
98
+
99
+ self.zones = Zones(self.client)
100
+ self.records = Records(self.client)
101
+
102
+ def close(self) -> None:
103
+ """Close the blocking HTTP client."""
104
+
105
+ self.client.get_httpx_client().close()
106
+
107
+ async def aclose(self) -> None:
108
+ """Close both clients, including any sync calls used in an async workflow."""
109
+
110
+ try:
111
+ await self.client.get_async_httpx_client().aclose()
112
+ finally:
113
+ self.close()
114
+
115
+ def __enter__(self) -> DNScale:
116
+ return self
117
+
118
+ def __exit__(self, *args: object) -> None:
119
+ self.close()
120
+
121
+ async def __aenter__(self) -> DNScale:
122
+ return self
123
+
124
+ async def __aexit__(self, *args: object) -> None:
125
+ await self.aclose()
dnscale/errors.py ADDED
@@ -0,0 +1,118 @@
1
+ """Exceptions raised by the handwritten SDK layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, TypeVar, cast
6
+
7
+ import httpx
8
+
9
+ from dnscale_api.models.error_response import ErrorResponse
10
+ from dnscale_api.types import Response, Unset
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class APIError(Exception):
16
+ """A structured error returned by DNScale."""
17
+
18
+ def __init__(
19
+ self,
20
+ *,
21
+ status_code: int,
22
+ code: str,
23
+ message: str,
24
+ details: list[dict[str, Any]] | None = None,
25
+ request_id: str | None = None,
26
+ ) -> None:
27
+ self.status_code = status_code
28
+ self.code = code
29
+ self.message = message
30
+ self.details = details
31
+ self.request_id = request_id
32
+ super().__init__(f"DNScale API error {status_code} ({code}): {message}")
33
+
34
+
35
+ class RateLimitError(APIError):
36
+ """The API still returned HTTP 429 after safe retries were exhausted."""
37
+
38
+ def __init__(self, *, retry_after: str | None = None, **kwargs: Any) -> None:
39
+ self.retry_after = retry_after
40
+ super().__init__(**kwargs)
41
+
42
+
43
+ class ProtocolError(Exception):
44
+ """The API returned a successful response that did not match its contract."""
45
+
46
+
47
+ def _check_error_body(response: httpx.Response) -> None:
48
+ try:
49
+ body = response.json()
50
+ if (
51
+ isinstance(body, dict)
52
+ and body.get("status") == "error"
53
+ and isinstance(body.get("error"), dict)
54
+ ):
55
+ error = body["error"]
56
+ if isinstance(error.get("code"), str) and isinstance(error.get("message"), str):
57
+ return
58
+ except ValueError:
59
+ pass
60
+ kwargs: dict[str, Any] = {
61
+ "status_code": response.status_code,
62
+ "code": "HTTP_ERROR",
63
+ "message": "DNScale returned an HTTP error without a valid API error envelope",
64
+ "request_id": response.headers.get("X-Request-ID"),
65
+ }
66
+ if response.status_code == 429:
67
+ raise RateLimitError(retry_after=response.headers.get("Retry-After"), **kwargs)
68
+ raise APIError(**kwargs)
69
+
70
+
71
+ def check_error_response(response: httpx.Response) -> None:
72
+ if response.status_code >= 400:
73
+ response.read()
74
+ _check_error_body(response)
75
+
76
+
77
+ async def acheck_error_response(response: httpx.Response) -> None:
78
+ if response.status_code >= 400:
79
+ await response.aread()
80
+ _check_error_body(response)
81
+
82
+
83
+ def unwrap(response: Response[T | ErrorResponse]) -> T | None:
84
+ """Return a successful parsed response or raise a structured SDK exception."""
85
+
86
+ parsed = response.parsed
87
+ if isinstance(parsed, ErrorResponse):
88
+ raw_details = parsed.error.details
89
+ details = None
90
+ if not isinstance(raw_details, Unset):
91
+ details = [item.to_dict() for item in raw_details]
92
+
93
+ error_type = RateLimitError if int(response.status_code) == 429 else APIError
94
+ kwargs: dict[str, Any] = {
95
+ "status_code": int(response.status_code),
96
+ "code": parsed.error.code,
97
+ "message": parsed.error.message,
98
+ "details": details,
99
+ "request_id": response.headers.get("X-Request-ID"),
100
+ }
101
+ if error_type is RateLimitError:
102
+ kwargs["retry_after"] = response.headers.get("Retry-After")
103
+ raise error_type(**kwargs)
104
+
105
+ if parsed is None and not 200 <= int(response.status_code) < 300:
106
+ raise ProtocolError(
107
+ f"DNScale returned HTTP {int(response.status_code)} without a valid error body"
108
+ )
109
+
110
+ return cast(T | None, parsed)
111
+
112
+
113
+ def expect(value: T | None, expected_type: type[T]) -> T:
114
+ """Narrow generated response unions and detect successful contract drift."""
115
+
116
+ if not isinstance(value, expected_type):
117
+ raise ProtocolError(f"DNScale returned an invalid {expected_type.__name__} response")
118
+ return value
dnscale/pagination.py ADDED
@@ -0,0 +1,20 @@
1
+ """Pagination guards shared by synchronous and asynchronous iterators."""
2
+
3
+ from dnscale_api.models.pagination import Pagination
4
+
5
+ from .errors import ProtocolError
6
+
7
+
8
+ def validate_page_size(page_size: int) -> None:
9
+ if not 1 <= page_size <= 100:
10
+ raise ValueError("page_size must be between 1 and 100")
11
+
12
+
13
+ def next_offset(page: Pagination, offset: int, count: int) -> int | None:
14
+ if page.offset != offset or page.count != count:
15
+ raise ProtocolError("DNScale returned inconsistent pagination metadata")
16
+ if not page.has_more:
17
+ return None
18
+ if count == 0:
19
+ raise ProtocolError("DNScale pagination has_more is true on an empty page")
20
+ return offset + count
dnscale/py.typed ADDED
@@ -0,0 +1 @@
1
+
dnscale/records.py ADDED
@@ -0,0 +1,295 @@
1
+ """Convenience methods for record CRUD."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncIterator, Iterator
6
+ from uuid import UUID
7
+
8
+ from dnscale_api import AuthenticatedClient
9
+ from dnscale_api.api.records import (
10
+ create_record,
11
+ delete_record,
12
+ delete_records_by_name_and_type,
13
+ get_record,
14
+ list_records,
15
+ update_record,
16
+ update_record_by_name_and_type,
17
+ )
18
+ from dnscale_api.models.create_record_request import CreateRecordRequest
19
+ from dnscale_api.models.record import Record
20
+ from dnscale_api.models.record_response import RecordResponse
21
+ from dnscale_api.models.record_type import RecordType
22
+ from dnscale_api.models.records_data import RecordsData
23
+ from dnscale_api.models.records_response import RecordsResponse
24
+ from dnscale_api.models.update_record_by_name_request import UpdateRecordByNameRequest
25
+ from dnscale_api.types import UNSET, Unset
26
+
27
+ from .errors import expect, unwrap
28
+ from .pagination import next_offset, validate_page_size
29
+ from .zones import _uuid
30
+
31
+
32
+ def _record_body(
33
+ *,
34
+ name: str,
35
+ record_type: RecordType | str,
36
+ content: str,
37
+ ttl: int,
38
+ priority: int | None | Unset,
39
+ disabled: bool,
40
+ comment: str | None | Unset,
41
+ ) -> CreateRecordRequest:
42
+ return CreateRecordRequest(
43
+ name=name,
44
+ type_=RecordType(record_type),
45
+ content=content,
46
+ ttl=ttl,
47
+ priority=priority,
48
+ disabled=disabled,
49
+ comment=comment,
50
+ )
51
+
52
+
53
+ class Records:
54
+ """Typed record operations bound to a DNScale client."""
55
+
56
+ def __init__(self, client: AuthenticatedClient) -> None:
57
+ self._client = client
58
+
59
+ def iter(self, zone_id: str | UUID, *, page_size: int = 100) -> Iterator[Record]:
60
+ """Lazily fetch all records, including server-limited pages."""
61
+ validate_page_size(page_size)
62
+ offset = 0
63
+ while True:
64
+ page = self.list(zone_id, offset=offset, limit=page_size)
65
+ following = next_offset(page.pagination, offset, len(page.records))
66
+ yield from page.records
67
+ if following is None:
68
+ return
69
+ offset = following
70
+
71
+ async def aiter(self, zone_id: str | UUID, *, page_size: int = 100) -> AsyncIterator[Record]:
72
+ validate_page_size(page_size)
73
+ offset = 0
74
+ while True:
75
+ page = await self.alist(zone_id, offset=offset, limit=page_size)
76
+ following = next_offset(page.pagination, offset, len(page.records))
77
+ for record in page.records:
78
+ yield record
79
+ if following is None:
80
+ return
81
+ offset = following
82
+
83
+ def update_by_name(
84
+ self,
85
+ zone_id: str | UUID,
86
+ name: str,
87
+ type_: RecordType | str,
88
+ *,
89
+ body: UpdateRecordByNameRequest,
90
+ content: str | Unset = UNSET,
91
+ ) -> Record:
92
+ """Update by owner name/type. See API semantics for multi-value RRsets."""
93
+ response = update_record_by_name_and_type.sync_detailed(
94
+ _uuid(zone_id),
95
+ name,
96
+ RecordType(type_),
97
+ client=self._client,
98
+ body=body,
99
+ content=content,
100
+ )
101
+ return expect(unwrap(response), RecordResponse).data.record
102
+
103
+ async def aupdate_by_name(
104
+ self,
105
+ zone_id: str | UUID,
106
+ name: str,
107
+ type_: RecordType | str,
108
+ *,
109
+ body: UpdateRecordByNameRequest,
110
+ content: str | Unset = UNSET,
111
+ ) -> Record:
112
+ response = await update_record_by_name_and_type.asyncio_detailed(
113
+ _uuid(zone_id),
114
+ name,
115
+ RecordType(type_),
116
+ client=self._client,
117
+ body=body,
118
+ content=content,
119
+ )
120
+ return expect(unwrap(response), RecordResponse).data.record
121
+
122
+ def delete_by_name(
123
+ self,
124
+ zone_id: str | UUID,
125
+ name: str,
126
+ type_: RecordType | str,
127
+ *,
128
+ content: str | Unset = UNSET,
129
+ ) -> None:
130
+ """Delete one value when content is supplied; otherwise delete the whole RRset."""
131
+ if content == "":
132
+ raise ValueError("content must be non-empty; omit it to delete the whole RRset")
133
+ unwrap(
134
+ delete_records_by_name_and_type.sync_detailed(
135
+ _uuid(zone_id), name, RecordType(type_), client=self._client, content=content
136
+ )
137
+ )
138
+
139
+ async def adelete_by_name(
140
+ self,
141
+ zone_id: str | UUID,
142
+ name: str,
143
+ type_: RecordType | str,
144
+ *,
145
+ content: str | Unset = UNSET,
146
+ ) -> None:
147
+ if content == "":
148
+ raise ValueError("content must be non-empty; omit it to delete the whole RRset")
149
+ unwrap(
150
+ await delete_records_by_name_and_type.asyncio_detailed(
151
+ _uuid(zone_id), name, RecordType(type_), client=self._client, content=content
152
+ )
153
+ )
154
+
155
+ def list(self, zone_id: str | UUID, *, offset: int = 0, limit: int = 50) -> RecordsData:
156
+ response = list_records.sync_detailed(
157
+ _uuid(zone_id), client=self._client, offset=offset, limit=limit
158
+ )
159
+ return expect(unwrap(response), RecordsResponse).data
160
+
161
+ async def alist(self, zone_id: str | UUID, *, offset: int = 0, limit: int = 50) -> RecordsData:
162
+ response = await list_records.asyncio_detailed(
163
+ _uuid(zone_id), client=self._client, offset=offset, limit=limit
164
+ )
165
+ return expect(unwrap(response), RecordsResponse).data
166
+
167
+ def get(self, zone_id: str | UUID, record_id: str) -> Record:
168
+ response = get_record.sync_detailed(_uuid(zone_id), record_id, client=self._client)
169
+ return expect(unwrap(response), RecordResponse).data.record
170
+
171
+ async def aget(self, zone_id: str | UUID, record_id: str) -> Record:
172
+ response = await get_record.asyncio_detailed(_uuid(zone_id), record_id, client=self._client)
173
+ return expect(unwrap(response), RecordResponse).data.record
174
+
175
+ def create(
176
+ self,
177
+ zone_id: str | UUID,
178
+ *,
179
+ name: str,
180
+ type_: RecordType | str,
181
+ content: str,
182
+ ttl: int = 3600,
183
+ priority: int | None | Unset = UNSET,
184
+ disabled: bool = False,
185
+ comment: str | None | Unset = UNSET,
186
+ ) -> Record:
187
+ response = create_record.sync_detailed(
188
+ _uuid(zone_id),
189
+ client=self._client,
190
+ body=_record_body(
191
+ name=name,
192
+ record_type=type_,
193
+ content=content,
194
+ ttl=ttl,
195
+ priority=priority,
196
+ disabled=disabled,
197
+ comment=comment,
198
+ ),
199
+ )
200
+ return expect(unwrap(response), RecordResponse).data.record
201
+
202
+ async def acreate(
203
+ self,
204
+ zone_id: str | UUID,
205
+ *,
206
+ name: str,
207
+ type_: RecordType | str,
208
+ content: str,
209
+ ttl: int = 3600,
210
+ priority: int | None | Unset = UNSET,
211
+ disabled: bool = False,
212
+ comment: str | None | Unset = UNSET,
213
+ ) -> Record:
214
+ response = await create_record.asyncio_detailed(
215
+ _uuid(zone_id),
216
+ client=self._client,
217
+ body=_record_body(
218
+ name=name,
219
+ record_type=type_,
220
+ content=content,
221
+ ttl=ttl,
222
+ priority=priority,
223
+ disabled=disabled,
224
+ comment=comment,
225
+ ),
226
+ )
227
+ return expect(unwrap(response), RecordResponse).data.record
228
+
229
+ def update(
230
+ self,
231
+ zone_id: str | UUID,
232
+ record_id: str,
233
+ *,
234
+ name: str,
235
+ type_: RecordType | str,
236
+ content: str,
237
+ ttl: int = 3600,
238
+ priority: int | None | Unset = UNSET,
239
+ disabled: bool = False,
240
+ comment: str | None | Unset = UNSET,
241
+ ) -> Record:
242
+ response = update_record.sync_detailed(
243
+ _uuid(zone_id),
244
+ record_id,
245
+ client=self._client,
246
+ body=_record_body(
247
+ name=name,
248
+ record_type=type_,
249
+ content=content,
250
+ ttl=ttl,
251
+ priority=priority,
252
+ disabled=disabled,
253
+ comment=comment,
254
+ ),
255
+ )
256
+ return expect(unwrap(response), RecordResponse).data.record
257
+
258
+ async def aupdate(
259
+ self,
260
+ zone_id: str | UUID,
261
+ record_id: str,
262
+ *,
263
+ name: str,
264
+ type_: RecordType | str,
265
+ content: str,
266
+ ttl: int = 3600,
267
+ priority: int | None | Unset = UNSET,
268
+ disabled: bool = False,
269
+ comment: str | None | Unset = UNSET,
270
+ ) -> Record:
271
+ response = await update_record.asyncio_detailed(
272
+ _uuid(zone_id),
273
+ record_id,
274
+ client=self._client,
275
+ body=_record_body(
276
+ name=name,
277
+ record_type=type_,
278
+ content=content,
279
+ ttl=ttl,
280
+ priority=priority,
281
+ disabled=disabled,
282
+ comment=comment,
283
+ ),
284
+ )
285
+ return expect(unwrap(response), RecordResponse).data.record
286
+
287
+ def delete(self, zone_id: str | UUID, record_id: str) -> None:
288
+ response = delete_record.sync_detailed(_uuid(zone_id), record_id, client=self._client)
289
+ unwrap(response)
290
+
291
+ async def adelete(self, zone_id: str | UUID, record_id: str) -> None:
292
+ response = await delete_record.asyncio_detailed(
293
+ _uuid(zone_id), record_id, client=self._client
294
+ )
295
+ unwrap(response)