automdev 0.1.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.
- autom/__init__.py +61 -0
- autom/_endpoints.py +200 -0
- autom/_operations.py +2058 -0
- autom/_response.py +82 -0
- autom/_serialization.py +105 -0
- autom/_version.py +1 -0
- autom/client.py +529 -0
- autom/errors.py +85 -0
- autom/models.py +81 -0
- autom/py.typed +1 -0
- automdev-0.1.0.dist-info/METADATA +218 -0
- automdev-0.1.0.dist-info/RECORD +14 -0
- automdev-0.1.0.dist-info/WHEEL +4 -0
- automdev-0.1.0.dist-info/licenses/LICENSE +21 -0
autom/_response.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Buffered HTTP response preservation and decoding."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .errors import AutomAPIError, AutomResponseDecodingError
|
|
12
|
+
from .models import APIResponse, BinaryResponse, JsonResponse, RawResponse
|
|
13
|
+
|
|
14
|
+
_ERROR_MESSAGE_KEYS = ("error", "message", "error_message", "detail")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_raw_response(response: httpx.Response, body: bytes) -> RawResponse:
|
|
18
|
+
grouped: dict[str, list[str]] = {}
|
|
19
|
+
for name, value in response.headers.multi_items():
|
|
20
|
+
grouped.setdefault(name.lower(), []).append(value)
|
|
21
|
+
return RawResponse(
|
|
22
|
+
status=response.status_code,
|
|
23
|
+
status_text=response.reason_phrase,
|
|
24
|
+
headers=MappingProxyType({name: tuple(values) for name, values in grouped.items()}),
|
|
25
|
+
content_type=response.headers.get("content-type"),
|
|
26
|
+
body=body,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_api_error(raw_response: RawResponse) -> AutomAPIError:
|
|
31
|
+
parsed, value = _try_parse_json(raw_response.body)
|
|
32
|
+
message = None
|
|
33
|
+
if parsed and isinstance(value, dict):
|
|
34
|
+
for key in _ERROR_MESSAGE_KEYS:
|
|
35
|
+
candidate = value.get(key)
|
|
36
|
+
if isinstance(candidate, str) and candidate:
|
|
37
|
+
message = candidate
|
|
38
|
+
break
|
|
39
|
+
return AutomAPIError(
|
|
40
|
+
raw_response=raw_response,
|
|
41
|
+
response_body=value if parsed else None,
|
|
42
|
+
error_message=message,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def decode_success_response(
|
|
47
|
+
raw_response: RawResponse, expected: Literal["json", "any"]
|
|
48
|
+
) -> APIResponse:
|
|
49
|
+
if not _is_json_media_type(raw_response.content_type):
|
|
50
|
+
if expected == "json":
|
|
51
|
+
raise AutomResponseDecodingError(
|
|
52
|
+
"Autom returned a successful non-JSON response for a JSON operation.",
|
|
53
|
+
raw_response,
|
|
54
|
+
TypeError(f"Unexpected content type: {raw_response.content_type}."),
|
|
55
|
+
)
|
|
56
|
+
return BinaryResponse(
|
|
57
|
+
bytes=raw_response.body,
|
|
58
|
+
content_type=raw_response.content_type,
|
|
59
|
+
raw_response=raw_response,
|
|
60
|
+
)
|
|
61
|
+
parsed, value = _try_parse_json(raw_response.body)
|
|
62
|
+
if not parsed:
|
|
63
|
+
raise AutomResponseDecodingError(
|
|
64
|
+
"Autom returned a successful JSON response that could not be decoded.",
|
|
65
|
+
raw_response,
|
|
66
|
+
value if isinstance(value, Exception) else None,
|
|
67
|
+
)
|
|
68
|
+
return JsonResponse(data=value, raw_response=raw_response)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _is_json_media_type(content_type: str | None) -> bool:
|
|
72
|
+
if content_type is None:
|
|
73
|
+
return False
|
|
74
|
+
media_type = content_type.split(";", 1)[0].strip().lower()
|
|
75
|
+
return media_type == "application/json" or media_type.endswith("+json")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _try_parse_json(body: bytes) -> tuple[bool, Any]:
|
|
79
|
+
try:
|
|
80
|
+
return True, json.loads(body)
|
|
81
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as cause:
|
|
82
|
+
return False, cause
|
autom/_serialization.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Request serialization: path filling, query pairs and JSON bodies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import quote, urlencode
|
|
12
|
+
|
|
13
|
+
from .errors import AutomSerializationError
|
|
14
|
+
from .models import HTTPMethod
|
|
15
|
+
|
|
16
|
+
_PATH_PARAM = re.compile(r"\{([^}]+)\}")
|
|
17
|
+
# The API reads the global parameters from the query string, whatever the method.
|
|
18
|
+
QUERY_STRING_PARAMS = ("fields", "is_async", "soft_fail")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class SerializedRequest:
|
|
23
|
+
path: str
|
|
24
|
+
query: list[tuple[str, str]]
|
|
25
|
+
body: dict[str, Any] | None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def serialize_request(
|
|
29
|
+
method: HTTPMethod, path_template: str, params: Mapping[str, Any] | None
|
|
30
|
+
) -> SerializedRequest:
|
|
31
|
+
"""Fill ``{name}`` path segments, then send the rest as query pairs (GET) or a JSON body (POST).
|
|
32
|
+
|
|
33
|
+
On POST, the global parameters (``fields``, ``is_async``, ``soft_fail``) still travel in the query string.
|
|
34
|
+
|
|
35
|
+
``None`` values and empty sequences are omitted; ``False`` and ``0`` are sent.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if params is not None and not isinstance(params, Mapping):
|
|
39
|
+
raise AutomSerializationError("Request parameters must be a mapping.")
|
|
40
|
+
remaining = {name: value for name, value in (params or {}).items() if value is not None}
|
|
41
|
+
|
|
42
|
+
def fill(match: re.Match[str]) -> str:
|
|
43
|
+
name = match.group(1)
|
|
44
|
+
if name not in remaining:
|
|
45
|
+
raise AutomSerializationError(f'The "{name}" path parameter is required.', name)
|
|
46
|
+
return quote(to_wire_string(name, remaining.pop(name)), safe="")
|
|
47
|
+
|
|
48
|
+
path = _PATH_PARAM.sub(fill, path_template)
|
|
49
|
+
|
|
50
|
+
if method == "GET":
|
|
51
|
+
query: list[tuple[str, str]] = []
|
|
52
|
+
for name, value in remaining.items():
|
|
53
|
+
if isinstance(value, (list, tuple)):
|
|
54
|
+
query.extend(
|
|
55
|
+
(name, to_wire_string(name, item)) for item in value if item is not None
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
query.append((name, to_wire_string(name, value)))
|
|
59
|
+
return SerializedRequest(path=path, query=query, body=None)
|
|
60
|
+
|
|
61
|
+
query = [
|
|
62
|
+
(name, to_wire_string(name, remaining.pop(name)))
|
|
63
|
+
for name in QUERY_STRING_PARAMS
|
|
64
|
+
if name in remaining
|
|
65
|
+
]
|
|
66
|
+
for name, value in remaining.items():
|
|
67
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
68
|
+
raise AutomSerializationError(f'The "{name}" parameter must be a finite number.', name)
|
|
69
|
+
return SerializedRequest(path=path, query=query, body=remaining)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def to_wire_string(name: str, value: Any) -> str:
|
|
73
|
+
if isinstance(value, bool):
|
|
74
|
+
return "true" if value else "false"
|
|
75
|
+
if isinstance(value, str):
|
|
76
|
+
return value
|
|
77
|
+
if isinstance(value, int):
|
|
78
|
+
return str(value)
|
|
79
|
+
if isinstance(value, float):
|
|
80
|
+
if not math.isfinite(value):
|
|
81
|
+
raise AutomSerializationError(f'The "{name}" parameter must be a finite number.', name)
|
|
82
|
+
return str(int(value)) if value.is_integer() else repr(value)
|
|
83
|
+
if isinstance(value, (Mapping, list, tuple)):
|
|
84
|
+
try:
|
|
85
|
+
return json.dumps(value, separators=(",", ":"), ensure_ascii=False)
|
|
86
|
+
except (TypeError, ValueError) as cause:
|
|
87
|
+
raise AutomSerializationError(
|
|
88
|
+
f'The "{name}" parameter could not be serialized.', name
|
|
89
|
+
) from cause
|
|
90
|
+
raise AutomSerializationError(
|
|
91
|
+
f'The "{name}" parameter has an unsupported type ({type(value).__name__}).', name
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def encode_query(pairs: list[tuple[str, str]]) -> str:
|
|
96
|
+
return urlencode(pairs, quote_via=quote)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def encode_json_body(body: Mapping[str, Any]) -> bytes:
|
|
100
|
+
try:
|
|
101
|
+
return json.dumps(body, ensure_ascii=False, allow_nan=False).encode("utf-8")
|
|
102
|
+
except (TypeError, ValueError) as cause:
|
|
103
|
+
raise AutomSerializationError(
|
|
104
|
+
"The request body could not be serialized as JSON."
|
|
105
|
+
) from cause
|
autom/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|