domainnameapi 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.
@@ -0,0 +1,38 @@
1
+ """DomainNameAPI client for Python.
2
+
3
+ Faithful port of php-dna (https://github.com/domainreseller/php-dna)::
4
+
5
+ from domainnameapi import DomainNameAPI
6
+
7
+ dna = DomainNameAPI("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "your-api-key")
8
+ print(dna.get_details("example.com"))
9
+ """
10
+
11
+ from ._version import VERSION, __version__
12
+ from .client import DomainNameAPI
13
+ from .rest import DNARest, FORWARD_TYPES, URL_OTE, URL_PROD, ZONE_RECORD_TYPES
14
+ from .shared import APPLICATION, RESULT_ERROR, RESULT_OK, USER_AGENT
15
+ from .transport import HttpRequest, HttpResponse, TransportError, UrllibTransport
16
+
17
+ #: Alias with php-dna's capitalisation.
18
+ DomainNameApi = DomainNameAPI
19
+
20
+ __all__ = [
21
+ "APPLICATION",
22
+ "DNARest",
23
+ "DomainNameAPI",
24
+ "DomainNameApi",
25
+ "FORWARD_TYPES",
26
+ "HttpRequest",
27
+ "HttpResponse",
28
+ "RESULT_ERROR",
29
+ "RESULT_OK",
30
+ "TransportError",
31
+ "URL_OTE",
32
+ "URL_PROD",
33
+ "USER_AGENT",
34
+ "UrllibTransport",
35
+ "VERSION",
36
+ "ZONE_RECORD_TYPES",
37
+ "__version__",
38
+ ]
domainnameapi/_php.py ADDED
@@ -0,0 +1,482 @@
1
+ """PHP value semantics used by php-dna, re-implemented for Python.
2
+
3
+ php-dna is the reference implementation. Its results depend on a handful of
4
+ PHP rules that differ from Python's: ``??`` treats ``null`` as missing,
5
+ ``empty()`` treats ``"0"`` as empty, ``(int)"12abc"`` is ``12``, string
6
+ functions such as ``strtolower`` and ``trim`` are ASCII/byte based, and so on.
7
+ Everything in this module exists so that the client returns exactly what
8
+ php-dna returns for the same input. It is internal API.
9
+ """
10
+
11
+ import calendar
12
+ import json
13
+ import math
14
+ import re
15
+ import time
16
+ from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
17
+ from urllib.parse import quote_plus
18
+
19
+ # Characters stripped by PHP's trim()/rtrim()/ltrim() without a char list.
20
+ PHP_TRIM_CHARS = " \t\n\r\0\x0b"
21
+
22
+ _ASCII_UPPER = str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")
23
+ _ASCII_LOWER = str.maketrans("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
24
+
25
+ _NUMERIC_RE = re.compile(r"^[ \t\n\r\x0b\f]*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?[ \t\n\r\x0b\f]*$")
26
+ _NUMERIC_PREFIX_RE = re.compile(r"^[ \t\n\r\x0b\f]*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)")
27
+ _INT_PREFIX_RE = re.compile(r"^[+-]?\d+$")
28
+
29
+
30
+ class JsonObject(dict):
31
+ """A dict that is always JSON-encoded as an object, even when empty.
32
+
33
+ PHP encodes an empty array as ``[]``; php-dna uses ``new stdClass()`` /
34
+ ``(object)`` where the gateway needs ``{}``. This marks those places.
35
+ """
36
+
37
+
38
+ _MISSING = object()
39
+
40
+
41
+ def get(value, *keys):
42
+ """Null-safe nested read with PHP ``isset``/``??`` semantics.
43
+
44
+ Returns ``None`` when any step is missing, ``None``, or not an array.
45
+ Integer and numeric-string keys are interchangeable, like PHP array keys.
46
+ """
47
+ for key in keys:
48
+ if isinstance(value, dict):
49
+ found = value.get(key, _MISSING)
50
+ if found is _MISSING:
51
+ if isinstance(key, int) and not isinstance(key, bool):
52
+ found = value.get(str(key), _MISSING)
53
+ elif isinstance(key, str) and re.match(r"^(0|-?[1-9]\d*)$", key):
54
+ found = value.get(int(key), _MISSING)
55
+ if found is _MISSING:
56
+ return None
57
+ value = found
58
+ elif isinstance(value, list):
59
+ idx = key
60
+ if isinstance(idx, str) and re.match(r"^(0|[1-9]\d*)$", idx):
61
+ idx = int(idx)
62
+ if isinstance(idx, int) and not isinstance(idx, bool) and 0 <= idx < len(value):
63
+ value = value[idx]
64
+ else:
65
+ return None
66
+ else:
67
+ return None
68
+ if value is None:
69
+ return None
70
+ return value
71
+
72
+
73
+ def coalesce(*values):
74
+ """PHP ``a ?? b ?? c``: the first value that is not ``None``."""
75
+ for v in values:
76
+ if v is not None:
77
+ return v
78
+ return None
79
+
80
+
81
+ def is_array(value):
82
+ return isinstance(value, (dict, list))
83
+
84
+
85
+ def empty(value):
86
+ """PHP ``empty()``."""
87
+ if value is None or value is False:
88
+ return True
89
+ if isinstance(value, bool):
90
+ return False
91
+ if isinstance(value, (int, float)):
92
+ return value == 0
93
+ if isinstance(value, str):
94
+ return value == "" or value == "0"
95
+ if isinstance(value, (list, dict, tuple)):
96
+ return len(value) == 0
97
+ return False
98
+
99
+
100
+ def truthy(value):
101
+ """PHP boolean conversion."""
102
+ return not empty(value)
103
+
104
+
105
+ def values(value):
106
+ """The values a PHP ``foreach`` over ``value`` would visit."""
107
+ if isinstance(value, dict):
108
+ return list(value.values())
109
+ if isinstance(value, (list, tuple)):
110
+ return list(value)
111
+ return []
112
+
113
+
114
+ def items(value):
115
+ """The ``key => value`` pairs a PHP ``foreach`` over ``value`` would visit."""
116
+ if isinstance(value, dict):
117
+ return list(value.items())
118
+ if isinstance(value, (list, tuple)):
119
+ return list(enumerate(value))
120
+ return []
121
+
122
+
123
+ def array_cast(value):
124
+ """PHP ``(array) $value``."""
125
+ if value is None:
126
+ return []
127
+ if isinstance(value, (dict, list)):
128
+ return value
129
+ if isinstance(value, tuple):
130
+ return list(value)
131
+ return [value]
132
+
133
+
134
+ def to_dict(value):
135
+ """A PHP array as an ordered ``dict`` (lists become ``{0: .., 1: ..}``)."""
136
+ if isinstance(value, dict):
137
+ return dict(value)
138
+ if isinstance(value, (list, tuple)):
139
+ return dict(enumerate(value))
140
+ if value is None:
141
+ return {}
142
+ return {0: value}
143
+
144
+
145
+ def float_to_string(value):
146
+ """PHP 8 ``(string)$float``: 14 significant digits (``precision`` ini),
147
+ exponent notation below 1e-4 and from 1e15 on (``1.0E+15``)."""
148
+ if math.isnan(value):
149
+ return "NAN"
150
+ if math.isinf(value):
151
+ return "INF" if value > 0 else "-INF"
152
+ if value == 0:
153
+ return "-0" if math.copysign(1.0, value) < 0 else "0"
154
+ precision = 14
155
+ mantissa, exp = ("%.*e" % (precision - 1, value)).split("e")
156
+ exp = int(exp)
157
+ if exp < -4 or exp >= precision:
158
+ if "." in mantissa:
159
+ mantissa = mantissa.rstrip("0").rstrip(".")
160
+ if "." not in mantissa:
161
+ mantissa += ".0"
162
+ return "%sE%s%d" % (mantissa, "+" if exp >= 0 else "-", abs(exp))
163
+ text = "%.*f" % (max(0, precision - 1 - exp), value)
164
+ if "." in text:
165
+ text = text.rstrip("0").rstrip(".")
166
+ return text
167
+
168
+
169
+ def strval(value):
170
+ """PHP ``(string) $value``."""
171
+ if value is None:
172
+ return ""
173
+ if value is True:
174
+ return "1"
175
+ if value is False:
176
+ return ""
177
+ if isinstance(value, int):
178
+ return str(value)
179
+ if isinstance(value, float):
180
+ return float_to_string(value)
181
+ if isinstance(value, str):
182
+ return value
183
+ if isinstance(value, (list, dict, tuple)):
184
+ return "Array"
185
+ return str(value)
186
+
187
+
188
+ def floatval(value):
189
+ """PHP ``(float) $value``."""
190
+ if value is None or value is False:
191
+ return 0.0
192
+ if value is True:
193
+ return 1.0
194
+ if isinstance(value, (int, float)):
195
+ return float(value)
196
+ if isinstance(value, str):
197
+ m = _NUMERIC_PREFIX_RE.match(value)
198
+ if not m:
199
+ return 0.0
200
+ try:
201
+ return float(m.group(1))
202
+ except (ValueError, OverflowError):
203
+ return 0.0
204
+ if isinstance(value, (list, dict, tuple)):
205
+ return 1.0 if value else 0.0
206
+ return 0.0
207
+
208
+
209
+ _INT_MAX = 2 ** 63 - 1
210
+ _INT_MIN = -(2 ** 63)
211
+
212
+
213
+ def _float_to_int(value, saturate=False):
214
+ if math.isnan(value) or math.isinf(value):
215
+ return 0 if not saturate or math.isnan(value) else (_INT_MAX if value > 0 else _INT_MIN)
216
+ if not (_INT_MIN <= value < 2.0 ** 63):
217
+ if saturate:
218
+ return _INT_MAX if value > 0 else _INT_MIN
219
+ # PHP 8 wraps out-of-range floats modulo 2**64.
220
+ r = int(value) % (2 ** 64)
221
+ return r - 2 ** 64 if r >= 2 ** 63 else r
222
+ return int(value)
223
+
224
+
225
+ def intval(value):
226
+ """PHP ``(int) $value``."""
227
+ if value is None or value is False:
228
+ return 0
229
+ if value is True:
230
+ return 1
231
+ if isinstance(value, int):
232
+ return value
233
+ if isinstance(value, float):
234
+ return _float_to_int(value)
235
+ if isinstance(value, str):
236
+ m = _NUMERIC_PREFIX_RE.match(value)
237
+ if not m:
238
+ return 0
239
+ num = m.group(1)
240
+ if _INT_PREFIX_RE.match(num):
241
+ return max(_INT_MIN, min(_INT_MAX, int(num)))
242
+ try:
243
+ return _float_to_int(float(num), saturate=True)
244
+ except (ValueError, OverflowError):
245
+ return 0
246
+ if isinstance(value, (list, dict, tuple)):
247
+ return 1 if value else 0
248
+ return 0
249
+
250
+
251
+ def filter_bool(value):
252
+ """``filter_var($value, FILTER_VALIDATE_BOOLEAN)``: "1"/"true"/"on"/"yes" -> True."""
253
+ if value is True:
254
+ return True
255
+ if value is None or value is False or isinstance(value, (list, dict, tuple)):
256
+ return False
257
+ return strval(value).strip(" \t\n\r\x0b\f\0").lower() in ("1", "true", "on", "yes")
258
+
259
+
260
+ def to_bool(value):
261
+ """PHP ``(bool) $value``."""
262
+ return truthy(value)
263
+
264
+
265
+ def is_numeric(value):
266
+ """PHP 8 ``is_numeric()``."""
267
+ if isinstance(value, bool):
268
+ return False
269
+ if isinstance(value, (int, float)):
270
+ return True
271
+ if isinstance(value, str):
272
+ return bool(_NUMERIC_RE.match(value))
273
+ return False
274
+
275
+
276
+ def trim(value, chars=PHP_TRIM_CHARS):
277
+ return strval(value).strip(chars)
278
+
279
+
280
+ def rtrim(value, chars=PHP_TRIM_CHARS):
281
+ return strval(value).rstrip(chars)
282
+
283
+
284
+ def ltrim(value, chars=PHP_TRIM_CHARS):
285
+ return strval(value).lstrip(chars)
286
+
287
+
288
+ def strtolower(value):
289
+ """ASCII-only lowercase, like PHP 8 ``strtolower``."""
290
+ return strval(value).translate(_ASCII_UPPER)
291
+
292
+
293
+ def strtoupper(value):
294
+ """ASCII-only uppercase, like PHP 8 ``strtoupper``."""
295
+ return strval(value).translate(_ASCII_LOWER)
296
+
297
+
298
+ def ucfirst(value):
299
+ s = strval(value)
300
+ if not s:
301
+ return s
302
+ return strtoupper(s[0]) + s[1:]
303
+
304
+
305
+ def strlen(value):
306
+ """Byte length (PHP strings are byte strings)."""
307
+ return len(strval(value).encode("utf-8"))
308
+
309
+
310
+ def strcasecmp_equal(a, b):
311
+ """``strcasecmp($a, $b) === 0``."""
312
+ return strtolower(a) == strtolower(b)
313
+
314
+
315
+ def stripos_found(haystack, needle):
316
+ """``stripos($haystack, $needle) !== false``."""
317
+ return strtolower(needle) in strtolower(haystack)
318
+
319
+
320
+ def loose_equals_string(a, b):
321
+ """PHP 8 ``==`` between two strings (numeric strings compare as numbers)."""
322
+ if a == b:
323
+ return True
324
+ if is_numeric(a) and is_numeric(b):
325
+ return float(a.strip(" \t\n\r\x0b\f")) == float(b.strip(" \t\n\r\x0b\f"))
326
+ return False
327
+
328
+
329
+ def number_format(value, decimals):
330
+ """``number_format($value, $decimals, '.', '')`` (round half away from zero)."""
331
+ value = float(value)
332
+ if math.isnan(value) or math.isinf(value):
333
+ return strval(value).lower() if math.isnan(value) else ("inf" if value > 0 else "-inf")
334
+ try:
335
+ d = Decimal(repr(value))
336
+ except InvalidOperation: # pragma: no cover - repr() of a finite float is always valid
337
+ d = Decimal(value)
338
+ q = Decimal(1).scaleb(-decimals)
339
+ rounded = d.quantize(q, rounding=ROUND_HALF_UP)
340
+ if rounded == 0:
341
+ rounded = abs(rounded)
342
+ return "{0:.{1}f}".format(rounded, decimals)
343
+
344
+
345
+ # --------------------------------------------------------------------------
346
+ # Query strings and JSON
347
+ # --------------------------------------------------------------------------
348
+
349
+ def urlencode(value):
350
+ """PHP ``urlencode()`` (spaces as ``+``, everything but ``A-Za-z0-9-_.`` escaped)."""
351
+ return quote_plus(strval(value), safe="").replace("~", "%7E")
352
+
353
+
354
+ def _query_scalar(value):
355
+ if value is True:
356
+ return "1"
357
+ if value is False:
358
+ return "0"
359
+ return strval(value)
360
+
361
+
362
+ def http_build_query(data):
363
+ """PHP ``http_build_query()``: ``true``/``false`` as ``1``/``0``, ``null`` skipped,
364
+ nested arrays as ``key[sub]=..`` (brackets percent-encoded)."""
365
+ parts = []
366
+
367
+ def walk(prefix, value):
368
+ if value is None:
369
+ return
370
+ if isinstance(value, (dict, list, tuple)):
371
+ for k, v in items(value):
372
+ walk("%s[%s]" % (prefix, strval(k)), v)
373
+ return
374
+ parts.append(urlencode(prefix) + "=" + urlencode(_query_scalar(value)))
375
+
376
+ for key, value in items(data):
377
+ if value is None:
378
+ continue
379
+ if isinstance(value, (dict, list, tuple)):
380
+ for k, v in items(value):
381
+ walk("%s[%s]" % (strval(key), strval(k)), v)
382
+ else:
383
+ parts.append(urlencode(strval(key)) + "=" + urlencode(_query_scalar(value)))
384
+ return "&".join(parts)
385
+
386
+
387
+ def _jsonable(value):
388
+ if isinstance(value, JsonObject):
389
+ return {strval(k): _jsonable(v) for k, v in value.items()}
390
+ if isinstance(value, dict):
391
+ if not value:
392
+ return []
393
+ keys = list(value.keys())
394
+ if all(isinstance(k, int) and not isinstance(k, bool) for k in keys) and keys == list(range(len(keys))):
395
+ return [_jsonable(v) for v in value.values()]
396
+ return {strval(k): _jsonable(v) for k, v in value.items()}
397
+ if isinstance(value, (list, tuple)):
398
+ return [_jsonable(v) for v in value]
399
+ return value
400
+
401
+
402
+ def json_encode(value):
403
+ """PHP ``json_encode()`` with default flags (``\\/`` and ``\\uXXXX`` escapes)."""
404
+ text = json.dumps(_jsonable(value), separators=(",", ":"), ensure_ascii=True, allow_nan=False)
405
+ return text.replace("/", "\\/")
406
+
407
+
408
+ def _reject_constant(name):
409
+ raise ValueError("invalid JSON constant " + name)
410
+
411
+
412
+ def json_decode(text):
413
+ """PHP ``json_decode($text, true)``: ``None`` for empty or invalid input."""
414
+ if text is None or text is False:
415
+ return None
416
+ if isinstance(text, bytes):
417
+ try:
418
+ text = text.decode("utf-8")
419
+ except UnicodeDecodeError:
420
+ return None
421
+ if not isinstance(text, str) or text.strip(" \t\n\r") == "":
422
+ return None
423
+ try:
424
+ return json.loads(text, parse_constant=_reject_constant)
425
+ except (ValueError, RecursionError):
426
+ return None
427
+
428
+
429
+ # --------------------------------------------------------------------------
430
+ # Dates
431
+ # --------------------------------------------------------------------------
432
+
433
+ _DATE_RE = re.compile(
434
+ r"^\s*(\d{4})-(\d{1,2})-(\d{1,2})"
435
+ r"(?:[Tt ]+(\d{1,2}):(\d{2})(?::(\d{2})(?:[.,](\d+))?)?)?"
436
+ r"\s*(Z|z|UTC|GMT|[+-]\d{2}:?\d{2}|[+-]\d{2})?\s*$"
437
+ )
438
+
439
+
440
+ def strtotime(value):
441
+ """Subset of PHP ``strtotime()`` covering the date formats the gateway emits.
442
+
443
+ Returns a Unix timestamp (UTC) or ``None`` when the value cannot be parsed
444
+ (PHP returns ``false``). Dates without an offset are read as UTC, the
445
+ timezone the conformance fixtures were generated in.
446
+ """
447
+ s = strval(value)
448
+ m = _DATE_RE.match(s)
449
+ if not m:
450
+ return None
451
+ year, month, day = int(m.group(1)), int(m.group(2)), int(m.group(3))
452
+ hour = int(m.group(4) or 0)
453
+ minute = int(m.group(5) or 0)
454
+ second = int(m.group(6) or 0)
455
+ if not (1 <= month <= 12) or not (1 <= day <= 31) or hour > 24 or minute > 59 or second > 60:
456
+ return None
457
+ # PHP rolls over out-of-range days (2023-02-30 -> 2023-03-02); timegm does too.
458
+ ts = calendar.timegm((year, month, 1, hour, minute, second, 0, 0, 0)) + (day - 1) * 86400
459
+ tz = m.group(8)
460
+ if tz and tz.upper() not in ("Z", "UTC", "GMT"):
461
+ sign = 1 if tz[0] == "+" else -1
462
+ digits = tz[1:].replace(":", "")
463
+ hours = int(digits[:2])
464
+ minutes = int(digits[2:4]) if len(digits) >= 4 else 0
465
+ ts -= sign * (hours * 3600 + minutes * 60)
466
+ return ts
467
+
468
+
469
+ def format_timestamp(ts):
470
+ """``date('Y-m-d\\TH:i:s', $ts)`` in UTC, years before 1000 zero-padded."""
471
+ days, rem = divmod(int(ts), 86400)
472
+ # 719163 = days from 0001-01-01 to 1970-01-01 (proleptic Gregorian ordinal).
473
+ from datetime import date
474
+
475
+ d = date.fromordinal(days + 719163)
476
+ hh, rem = divmod(rem, 3600)
477
+ mm, ss = divmod(rem, 60)
478
+ return "%04d-%02d-%02dT%02d:%02d:%02d" % (d.year, d.month, d.day, hh, mm, ss)
479
+
480
+
481
+ def now():
482
+ return time.time()
@@ -0,0 +1,4 @@
1
+ """Library version (single source of truth, also read by hatchling)."""
2
+
3
+ __version__ = "1.0.0"
4
+ VERSION = __version__