comatype 0.0.1__tar.gz
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.
- comatype-0.0.1/PKG-INFO +23 -0
- comatype-0.0.1/README.md +7 -0
- comatype-0.0.1/comatype/__init__.py +35 -0
- comatype-0.0.1/comatype/cache.py +118 -0
- comatype-0.0.1/comatype/client.py +546 -0
- comatype-0.0.1/comatype/cookie.py +139 -0
- comatype-0.0.1/comatype/endpoint.py +101 -0
- comatype-0.0.1/comatype/errors.py +25 -0
- comatype-0.0.1/comatype/py.typed +0 -0
- comatype-0.0.1/comatype/raw.py +67 -0
- comatype-0.0.1/comatype/scalars.py +124 -0
- comatype-0.0.1/comatype/server.py +307 -0
- comatype-0.0.1/comatype/wire.py +146 -0
- comatype-0.0.1/comatype.egg-info/PKG-INFO +23 -0
- comatype-0.0.1/comatype.egg-info/SOURCES.txt +20 -0
- comatype-0.0.1/comatype.egg-info/dependency_links.txt +1 -0
- comatype-0.0.1/comatype.egg-info/requires.txt +2 -0
- comatype-0.0.1/comatype.egg-info/top_level.txt +1 -0
- comatype-0.0.1/pyproject.toml +36 -0
- comatype-0.0.1/setup.cfg +4 -0
- comatype-0.0.1/tests/test_cache.py +43 -0
- comatype-0.0.1/tests/test_runtime.py +407 -0
comatype-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: comatype
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Comatype runtime for Python (client `c` and server `s`)
|
|
5
|
+
Author: oldsheeep3
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/oldsheeep3/comatype
|
|
8
|
+
Project-URL: Repository, https://github.com/oldsheeep3/comatype
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: pydantic>=2.7
|
|
15
|
+
Requires-Dist: httpx>=0.27
|
|
16
|
+
|
|
17
|
+
# comatype
|
|
18
|
+
|
|
19
|
+
Runtime for Python code generated by [Comatype](https://github.com/oldsheeep3/comatype): the client `c` and the server `s`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from comatype import c, s
|
|
23
|
+
```
|
comatype-0.0.1/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Comatype runtime: provides the client `c` and server `s` used by generated code (comatype_gen)."""
|
|
2
|
+
|
|
3
|
+
from . import scalars
|
|
4
|
+
from .cache import CacheSpec, MemoryCache
|
|
5
|
+
from .client import Client, c
|
|
6
|
+
from .cookie import Cookie, CookieDef, CookieJar, serialize_set_cookie
|
|
7
|
+
from .endpoint import ComatypeWrap, Endpoint, NoArg, Status
|
|
8
|
+
from .errors import ComatypeError, ComatypeValidationError, UnexpectedStatusError
|
|
9
|
+
from .raw import CtxT, Headers, RawRequest, RawResponse
|
|
10
|
+
from .server import Server, s
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CacheSpec",
|
|
14
|
+
"MemoryCache",
|
|
15
|
+
"Client",
|
|
16
|
+
"ComatypeError",
|
|
17
|
+
"ComatypeValidationError",
|
|
18
|
+
"ComatypeWrap",
|
|
19
|
+
"Cookie",
|
|
20
|
+
"CookieDef",
|
|
21
|
+
"CookieJar",
|
|
22
|
+
"CtxT",
|
|
23
|
+
"Endpoint",
|
|
24
|
+
"Headers",
|
|
25
|
+
"NoArg",
|
|
26
|
+
"RawRequest",
|
|
27
|
+
"RawResponse",
|
|
28
|
+
"Server",
|
|
29
|
+
"Status",
|
|
30
|
+
"UnexpectedStatusError",
|
|
31
|
+
"c",
|
|
32
|
+
"s",
|
|
33
|
+
"scalars",
|
|
34
|
+
"serialize_set_cookie",
|
|
35
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Cache (`cache` in `.comat`): an in-memory TTL cache and the encoding used to store entries in cookies. Applies to GET / QUERY only.。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import time
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from .raw import Headers, RawResponse
|
|
13
|
+
|
|
14
|
+
# Name prefix of cache cookies. The client does not send cookies with this prefix back to the server.
|
|
15
|
+
CACHE_COOKIE_PREFIX = "comatype_cache_"
|
|
16
|
+
# Maximum length of one cookie value. Larger responses are not cached in a cookie.
|
|
17
|
+
CACHE_COOKIE_MAX = 3800
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class CacheSpec:
|
|
22
|
+
"""The `cache` setting. mode is off (never cache) / memory (keep in memory for ttl) / cookie (keep in a cookie for ttl; client only)."""
|
|
23
|
+
|
|
24
|
+
mode: Literal["off", "memory", "cookie"]
|
|
25
|
+
ttl_ms: int = 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def memory(self) -> bool:
|
|
29
|
+
return self.mode == "memory" and self.ttl_ms > 0
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def cookie(self) -> bool:
|
|
33
|
+
return self.mode == "cookie" and self.ttl_ms > 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _clone(r: RawResponse) -> RawResponse:
|
|
37
|
+
return RawResponse(status=r.status, headers=Headers(dict(r.headers.items())), body=r.body)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MemoryCache:
|
|
41
|
+
"""In-memory cache with a ttl (expired entries are dropped on lookup; over the size limit, the oldest are dropped first)."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, max_entries: int = 1000, now: Callable[[], float] = time.monotonic) -> None:
|
|
44
|
+
self._max = max_entries
|
|
45
|
+
self._now = now
|
|
46
|
+
self._d: OrderedDict[str, tuple[RawResponse, float]] = OrderedDict()
|
|
47
|
+
|
|
48
|
+
def get(self, key: str) -> RawResponse | None:
|
|
49
|
+
e = self._d.get(key)
|
|
50
|
+
if e is None:
|
|
51
|
+
return None
|
|
52
|
+
if e[1] <= self._now():
|
|
53
|
+
del self._d[key]
|
|
54
|
+
return None
|
|
55
|
+
return _clone(e[0])
|
|
56
|
+
|
|
57
|
+
def set(self, key: str, res: RawResponse, ttl_ms: int) -> None:
|
|
58
|
+
self._d.pop(key, None)
|
|
59
|
+
self._d[key] = (_clone(res), self._now() + ttl_ms / 1000)
|
|
60
|
+
while len(self._d) > self._max:
|
|
61
|
+
self._d.popitem(last=False)
|
|
62
|
+
|
|
63
|
+
def clear(self) -> None:
|
|
64
|
+
self._d.clear()
|
|
65
|
+
|
|
66
|
+
def __len__(self) -> int:
|
|
67
|
+
return len(self._d)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cacheable(res: RawResponse) -> bool:
|
|
71
|
+
"""Whether a response may be cached (2xx, no Set-Cookie)."""
|
|
72
|
+
return 200 <= res.status < 300 and not res.set_cookies
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def client_cache_key(method: str, url: str, body: bytes, declared: tuple[str, ...], headers: Mapping[str, str]) -> str:
|
|
76
|
+
"""Client cache key: method, URL (path and query) and body, plus the request headers declared in `.comat`.
|
|
77
|
+
Headers added by wrappers or init (Authorization etc.) are not included."""
|
|
78
|
+
lower = {k.lower(): v for k, v in headers.items()}
|
|
79
|
+
hs = "\n".join(f"{n}:{lower.get(n, '')}" for n in sorted(h.lower() for h in declared))
|
|
80
|
+
return f"{method}\n{url}\n{hs}\n\n{body.decode('utf-8', 'surrogateescape')}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def server_cache_key(method: str, target: str, headers: Mapping[str, str], body: bytes) -> str:
|
|
84
|
+
"""Server cache key: an exact match of the request (method, path, query, all headers, body)."""
|
|
85
|
+
hs = "\n".join(f"{k.lower()}:{v}" for k, v in sorted(headers.items(), key=lambda kv: kv[0].lower()))
|
|
86
|
+
return f"{method}\n{target}\n{hs}\n\n{body.decode('utf-8', 'surrogateescape')}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cache_cookie_name(key: str) -> str:
|
|
90
|
+
"""Build a cookie name from the key (FNV-1a, 64-bit)."""
|
|
91
|
+
h = 0xCBF29CE484222325
|
|
92
|
+
for b in key.encode("utf-8", "surrogateescape"):
|
|
93
|
+
h = ((h ^ b) * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
|
|
94
|
+
return f"{CACHE_COOKIE_PREFIX}{h:016x}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def encode_cache_cookie(res: RawResponse) -> str | None:
|
|
98
|
+
"""Encode a response as a cookie value (base64url of `status\\nname\\tvalue\\n...\\n\\nbody`). None if too large."""
|
|
99
|
+
head = f"{res.status}\n" + "".join(f"{k}\t{v}\n" for k, v in res.headers.items())
|
|
100
|
+
v = base64.urlsafe_b64encode(head.encode() + b"\n" + res.body).decode().rstrip("=")
|
|
101
|
+
return v if len(v) <= CACHE_COOKIE_MAX else None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def decode_cache_cookie(value: str) -> RawResponse | None:
|
|
105
|
+
try:
|
|
106
|
+
raw = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
|
|
107
|
+
head, sep, body = raw.partition(b"\n\n")
|
|
108
|
+
if not sep:
|
|
109
|
+
return None
|
|
110
|
+
lines = head.decode().split("\n")
|
|
111
|
+
out = RawResponse(status=int(lines[0]), body=body)
|
|
112
|
+
for ln in lines[1:]:
|
|
113
|
+
k, tab, v = ln.partition("\t")
|
|
114
|
+
if tab:
|
|
115
|
+
out.headers[k] = v
|
|
116
|
+
return out
|
|
117
|
+
except Exception: # noqa: BLE001
|
|
118
|
+
return None
|