bytesmap 0.1.1__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.
- bytesmap/__init__.py +23 -0
- bytesmap/_catalog.json +2192 -0
- bytesmap/_generated.py +804 -0
- bytesmap/_limits.json +9 -0
- bytesmap/_transport.py +120 -0
- bytesmap/client.py +214 -0
- bytesmap/events.py +435 -0
- bytesmap/py.typed +0 -0
- bytesmap-0.1.1.dist-info/METADATA +133 -0
- bytesmap-0.1.1.dist-info/RECORD +12 -0
- bytesmap-0.1.1.dist-info/WHEEL +4 -0
- bytesmap-0.1.1.dist-info/licenses/LICENSE +202 -0
bytesmap/_limits.json
ADDED
bytesmap/_transport.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Standard-library transport with verified TLS and no redirect replay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import math
|
|
7
|
+
import ssl
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from http.client import HTTPException
|
|
11
|
+
from typing import Protocol
|
|
12
|
+
from urllib.error import HTTPError, URLError
|
|
13
|
+
from urllib.parse import urlsplit
|
|
14
|
+
from urllib.request import HTTPRedirectHandler, HTTPSHandler, Request, build_opener
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Response:
|
|
19
|
+
status: int
|
|
20
|
+
headers: Mapping[str, str]
|
|
21
|
+
body: bytes
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Transport(Protocol):
|
|
25
|
+
def request(
|
|
26
|
+
self,
|
|
27
|
+
method: str,
|
|
28
|
+
url: str,
|
|
29
|
+
headers: Mapping[str, str],
|
|
30
|
+
body: bytes | None,
|
|
31
|
+
timeout: float,
|
|
32
|
+
) -> Response: ...
|
|
33
|
+
def close(self) -> None: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TransportError(Exception):
|
|
37
|
+
"""Network delivery could not be confirmed; credentials are never included."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def validate_origin(value: str) -> tuple[str, bool]:
|
|
41
|
+
if not isinstance(value, str) or any(
|
|
42
|
+
ord(char) < 33 or ord(char) == 127 for char in value
|
|
43
|
+
):
|
|
44
|
+
raise ValueError("Endpoint must be an HTTPS origin or HTTP loopback")
|
|
45
|
+
try:
|
|
46
|
+
parsed = urlsplit(value)
|
|
47
|
+
port = parsed.port
|
|
48
|
+
host = parsed.hostname
|
|
49
|
+
if not host or port is not None and not 1 <= port <= 65535:
|
|
50
|
+
raise ValueError()
|
|
51
|
+
try:
|
|
52
|
+
loopback = ipaddress.ip_address(host).is_loopback
|
|
53
|
+
except ValueError:
|
|
54
|
+
loopback = host.lower() == "localhost"
|
|
55
|
+
if (
|
|
56
|
+
parsed.scheme != "https"
|
|
57
|
+
and not (parsed.scheme == "http" and loopback)
|
|
58
|
+
or parsed.username is not None
|
|
59
|
+
or parsed.password is not None
|
|
60
|
+
or parsed.path not in ("", "/")
|
|
61
|
+
or parsed.query
|
|
62
|
+
or parsed.fragment
|
|
63
|
+
):
|
|
64
|
+
raise ValueError()
|
|
65
|
+
except ValueError:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"Endpoint must be an HTTPS origin or HTTP loopback without credentials, path, query or fragment"
|
|
68
|
+
) from None
|
|
69
|
+
return value.rstrip("/"), parsed.scheme == "http"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def validate_timeout(value: float) -> None:
|
|
73
|
+
if (
|
|
74
|
+
isinstance(value, bool)
|
|
75
|
+
or not isinstance(value, (int, float))
|
|
76
|
+
or not math.isfinite(value)
|
|
77
|
+
or value <= 0
|
|
78
|
+
):
|
|
79
|
+
raise ValueError("Timeout must be a finite positive number")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _NoRedirect(HTTPRedirectHandler):
|
|
83
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class UrllibTransport:
|
|
88
|
+
def __init__(self) -> None:
|
|
89
|
+
self._opener = build_opener(
|
|
90
|
+
_NoRedirect(), HTTPSHandler(context=ssl.create_default_context())
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def request(
|
|
94
|
+
self,
|
|
95
|
+
method: str,
|
|
96
|
+
url: str,
|
|
97
|
+
headers: Mapping[str, str],
|
|
98
|
+
body: bytes | None,
|
|
99
|
+
timeout: float,
|
|
100
|
+
) -> Response:
|
|
101
|
+
request = Request(url, data=body, headers=dict(headers), method=method)
|
|
102
|
+
try:
|
|
103
|
+
try:
|
|
104
|
+
response = self._opener.open(request, timeout=timeout)
|
|
105
|
+
except HTTPError as error:
|
|
106
|
+
response = error
|
|
107
|
+
with response:
|
|
108
|
+
payload = response.read(64 * 1024 * 1024 + 1)
|
|
109
|
+
if len(payload) > 64 * 1024 * 1024:
|
|
110
|
+
raise TransportError("Response exceeded the SDK size limit")
|
|
111
|
+
return Response(
|
|
112
|
+
response.status,
|
|
113
|
+
{key.lower(): value for key, value in response.headers.items()},
|
|
114
|
+
payload,
|
|
115
|
+
)
|
|
116
|
+
except (URLError, OSError, ValueError, HTTPException):
|
|
117
|
+
raise TransportError("BytesMap connection failed") from None
|
|
118
|
+
|
|
119
|
+
def close(self) -> None:
|
|
120
|
+
self._opener.close()
|
bytesmap/client.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Workspace-scoped operations generated from BytesMap's shared catalogue."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from importlib.resources import files
|
|
11
|
+
from typing import Any, Self
|
|
12
|
+
from urllib.parse import quote, urlencode
|
|
13
|
+
|
|
14
|
+
from ._generated import GeneratedClient
|
|
15
|
+
from ._transport import Transport, UrllibTransport, validate_origin, validate_timeout
|
|
16
|
+
|
|
17
|
+
_CATALOG = json.loads(files(__package__).joinpath("_catalog.json").read_text("utf-8"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BytesMapError(Exception):
|
|
21
|
+
def __init__(
|
|
22
|
+
self, code: str, params: Mapping[str, Any] | None = None, status: int = 0
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__(code)
|
|
25
|
+
self.code, self.params, self.status = code, dict(params or {}), status
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class CallOptions:
|
|
30
|
+
project_id: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _json(value: Any) -> bytes:
|
|
34
|
+
return json.dumps(
|
|
35
|
+
value, ensure_ascii=False, allow_nan=False, separators=(",", ":")
|
|
36
|
+
).encode("utf-8")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _header(value: str) -> str:
|
|
40
|
+
if not isinstance(value, str) or any(
|
|
41
|
+
ord(char) < 32 or ord(char) == 127 for char in value
|
|
42
|
+
):
|
|
43
|
+
raise ValueError("Invalid authentication or scope header")
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BytesMapClient(GeneratedClient):
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
base_url: str,
|
|
51
|
+
*,
|
|
52
|
+
token: str | Callable[[], str | None] | None = None,
|
|
53
|
+
organization_id: str | None = None,
|
|
54
|
+
project_id: str | Callable[[], str | None] | None = None,
|
|
55
|
+
timeout: float = 30,
|
|
56
|
+
transport: Transport | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._base_url, _ = validate_origin(base_url)
|
|
59
|
+
validate_timeout(timeout)
|
|
60
|
+
self._token, self._organization_id, self._project_id = (
|
|
61
|
+
token,
|
|
62
|
+
organization_id,
|
|
63
|
+
project_id,
|
|
64
|
+
)
|
|
65
|
+
self._timeout = timeout
|
|
66
|
+
self._transport = transport or UrllibTransport()
|
|
67
|
+
self._owns_transport = transport is None
|
|
68
|
+
self._closed = False
|
|
69
|
+
self._owner: BytesMapClient | None = None
|
|
70
|
+
self._initialize_modules()
|
|
71
|
+
|
|
72
|
+
def scope(self, organization_id: str) -> BytesMapClient:
|
|
73
|
+
self._check_open()
|
|
74
|
+
if not organization_id:
|
|
75
|
+
raise ValueError("Workspace ID is required")
|
|
76
|
+
scoped = BytesMapClient(
|
|
77
|
+
self._base_url,
|
|
78
|
+
token=self._token,
|
|
79
|
+
organization_id=_header(organization_id),
|
|
80
|
+
project_id=self._project_id,
|
|
81
|
+
timeout=self._timeout,
|
|
82
|
+
transport=self._transport,
|
|
83
|
+
)
|
|
84
|
+
scoped._owner = self._owner or self
|
|
85
|
+
return scoped
|
|
86
|
+
|
|
87
|
+
def invoke(
|
|
88
|
+
self,
|
|
89
|
+
operation: str,
|
|
90
|
+
input: Mapping[str, Any] | None = None,
|
|
91
|
+
*,
|
|
92
|
+
call_options: CallOptions | None = None,
|
|
93
|
+
**kwargs: Any,
|
|
94
|
+
) -> Any:
|
|
95
|
+
self._check_open()
|
|
96
|
+
if operation not in _CATALOG:
|
|
97
|
+
raise ValueError("Unknown BytesMap operation")
|
|
98
|
+
spec = _CATALOG[operation]
|
|
99
|
+
if input is not None and not isinstance(input, Mapping):
|
|
100
|
+
raise TypeError("Operation input must be a mapping")
|
|
101
|
+
values = dict(input or {})
|
|
102
|
+
for key, value in kwargs.items():
|
|
103
|
+
wire_key = spec["aliases"].get(key, key)
|
|
104
|
+
if wire_key in values:
|
|
105
|
+
raise ValueError("Duplicate operation input field")
|
|
106
|
+
values[wire_key] = value
|
|
107
|
+
values = json.loads(_json(values))
|
|
108
|
+
# Resolve project selection before a token callback can change application state.
|
|
109
|
+
project = (
|
|
110
|
+
call_options.project_id
|
|
111
|
+
if call_options and call_options.project_id is not None
|
|
112
|
+
else (
|
|
113
|
+
self._project_id() if callable(self._project_id) else self._project_id
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def segment(match):
|
|
118
|
+
key = match.group(1)
|
|
119
|
+
if (
|
|
120
|
+
key not in values
|
|
121
|
+
or values[key] is None
|
|
122
|
+
or isinstance(values[key], (dict, list))
|
|
123
|
+
):
|
|
124
|
+
raise ValueError("Missing or invalid path parameter")
|
|
125
|
+
value = values.pop(key)
|
|
126
|
+
text = value if isinstance(value, str) else _json(value).decode("utf-8")
|
|
127
|
+
if not text or text in (".", ".."):
|
|
128
|
+
raise ValueError("Invalid path parameter")
|
|
129
|
+
return quote(text, safe="")
|
|
130
|
+
|
|
131
|
+
path = re.sub(r":([A-Za-z0-9_]+)", segment, spec["path"])
|
|
132
|
+
url = self._base_url + path
|
|
133
|
+
headers = {"accept": "application/json"}
|
|
134
|
+
token = self._token() if callable(self._token) else self._token
|
|
135
|
+
if token:
|
|
136
|
+
headers["authorization"] = "Bearer " + _header(token)
|
|
137
|
+
if self._organization_id:
|
|
138
|
+
headers["x-organization-id"] = _header(self._organization_id)
|
|
139
|
+
if project:
|
|
140
|
+
headers["x-bytesmap-project"] = _header(project)
|
|
141
|
+
body = None
|
|
142
|
+
if spec["method"] == "GET":
|
|
143
|
+
if values:
|
|
144
|
+
query = {
|
|
145
|
+
key: value
|
|
146
|
+
if isinstance(value, str)
|
|
147
|
+
else _json(value).decode("utf-8")
|
|
148
|
+
for key, value in values.items()
|
|
149
|
+
}
|
|
150
|
+
url += "?" + urlencode(query)
|
|
151
|
+
else:
|
|
152
|
+
body = _json(values)
|
|
153
|
+
headers["content-type"] = "application/json"
|
|
154
|
+
response = self._transport.request(
|
|
155
|
+
spec["method"], url, headers, body, self._timeout
|
|
156
|
+
)
|
|
157
|
+
if not 200 <= response.status < 300:
|
|
158
|
+
try:
|
|
159
|
+
error = json.loads(response.body)
|
|
160
|
+
except (ValueError, UnicodeError):
|
|
161
|
+
error = {}
|
|
162
|
+
if not isinstance(error, dict):
|
|
163
|
+
error = {}
|
|
164
|
+
raise BytesMapError(
|
|
165
|
+
error.get("code")
|
|
166
|
+
if isinstance(error.get("code"), str)
|
|
167
|
+
else f"http.{response.status}",
|
|
168
|
+
error.get("params") if isinstance(error.get("params"), dict) else {},
|
|
169
|
+
response.status,
|
|
170
|
+
)
|
|
171
|
+
if spec["response"] == "file":
|
|
172
|
+
headers = {key.lower(): value for key, value in response.headers.items()}
|
|
173
|
+
disposition = headers.get("content-disposition", "")
|
|
174
|
+
from urllib.parse import unquote
|
|
175
|
+
|
|
176
|
+
filename = re.search(
|
|
177
|
+
r"filename\*=UTF-8''([^;]+)", disposition, re.IGNORECASE
|
|
178
|
+
)
|
|
179
|
+
plain = re.search(r'filename="?([^";]+)', disposition, re.IGNORECASE)
|
|
180
|
+
return {
|
|
181
|
+
"filename": unquote(filename.group(1))
|
|
182
|
+
if filename
|
|
183
|
+
else plain.group(1)
|
|
184
|
+
if plain
|
|
185
|
+
else "report",
|
|
186
|
+
"contentType": headers.get("content-type", "application/octet-stream"),
|
|
187
|
+
"contentBase64": base64.b64encode(response.body).decode("ascii"),
|
|
188
|
+
}
|
|
189
|
+
try:
|
|
190
|
+
result = json.loads(response.body)
|
|
191
|
+
except (ValueError, UnicodeError):
|
|
192
|
+
raise BytesMapError("response.invalid", status=response.status) from None
|
|
193
|
+
if spec["response"] == "raw":
|
|
194
|
+
return result
|
|
195
|
+
if not isinstance(result, dict) or "data" not in result:
|
|
196
|
+
raise BytesMapError("response.invalid", status=response.status)
|
|
197
|
+
return result["data"]
|
|
198
|
+
|
|
199
|
+
def _check_open(self) -> None:
|
|
200
|
+
if self._closed or self._owner is not None and self._owner._closed:
|
|
201
|
+
raise RuntimeError("BytesMap client is closed")
|
|
202
|
+
|
|
203
|
+
def close(self) -> None:
|
|
204
|
+
if not self._closed:
|
|
205
|
+
self._closed = True
|
|
206
|
+
if self._owns_transport:
|
|
207
|
+
self._transport.close()
|
|
208
|
+
|
|
209
|
+
def __enter__(self) -> Self:
|
|
210
|
+
self._check_open()
|
|
211
|
+
return self
|
|
212
|
+
|
|
213
|
+
def __exit__(self, *_: object) -> None:
|
|
214
|
+
self.close()
|