tigrbl-atoms 0.1.0.dev5__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.
- tigrbl_atoms-0.1.0.dev5/PKG-INFO +55 -0
- tigrbl_atoms-0.1.0.dev5/README.md +29 -0
- tigrbl_atoms-0.1.0.dev5/pyproject.toml +51 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/__init__.py +25 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/_ctx.py +23 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/_opview_helpers.py +161 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/_request.py +159 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/algebra.py +239 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/__init__.py +111 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/_temp.py +91 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dep/__init__.py +15 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dep/_param_resolver.py +96 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dep/extra.py +121 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dep/security.py +131 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dispatch/__init__.py +19 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dispatch/binding_match.py +166 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dispatch/binding_parse.py +243 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dispatch/input_normalize.py +63 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/dispatch/op_resolve.py +102 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/__init__.py +48 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/asgi_send.py +347 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/envelope_apply.py +108 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/headers_apply.py +65 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/http_finalize.py +50 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/out_dump.py +40 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/result_normalize.py +44 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/egress/to_transport_response.py +183 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/emit/__init__.py +42 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/emit/paired_post.py +165 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/emit/paired_pre.py +120 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/emit/readtime_alias.py +133 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/err/__init__.py +3 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/err/rollback.py +29 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/ingress/__init__.py +35 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/ingress/ctx_init.py +34 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/ingress/input_prepare.py +50 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/ingress/transport_extract.py +190 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/out/__init__.py +38 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/out/masking.py +149 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/refresh/__init__.py +38 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/refresh/demand.py +144 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/resolve/__init__.py +40 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/resolve/assemble.py +164 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/resolve/paired_gen.py +163 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/__init__.py +23 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/error_to_transport.py +48 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/headers_from_payload.py +73 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/negotiate.py +44 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/negotiation.py +43 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/render.py +100 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/renderer.py +199 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/template.py +58 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/response/templates.py +90 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/schema/__init__.py +40 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/schema/collect_in.py +43 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/schema/collect_out.py +43 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/storage/__init__.py +38 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/storage/to_stored.py +181 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/__init__.py +79 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/_db.py +45 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/_oltp_context.py +214 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/commit_tx.py +49 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_bulk_create.py +36 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_bulk_delete.py +48 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_bulk_merge.py +36 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_bulk_replace.py +36 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_bulk_update.py +36 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_clear.py +33 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_create.py +35 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_custom.py +74 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_delete.py +34 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_list.py +104 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_merge.py +35 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_noop.py +38 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_persistence.py +111 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_read.py +34 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_replace.py +35 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/handler_update.py +35 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/phase_db.py +157 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/sys/start_tx.py +49 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/wire/__init__.py +45 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/wire/build_in.py +350 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/wire/build_out.py +125 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/wire/dump.py +284 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/atoms/wire/validate_in.py +272 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/events.py +395 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/phases.py +234 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/stages.py +109 -0
- tigrbl_atoms-0.1.0.dev5/tigrbl_atoms/types.py +403 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tigrbl-atoms
|
|
3
|
+
Version: 0.1.0.dev5
|
|
4
|
+
Summary: Runtime atom utilities and execution helpers for Tigrbl.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Keywords: tigrbl,sdk,standards,framework
|
|
7
|
+
Author: Jacob Stewart
|
|
8
|
+
Author-email: jacob@swarmauri.com
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Development Status :: 1 - Planning
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Requires-Dist: jinja2 (>=3.1)
|
|
19
|
+
Requires-Dist: sqlalchemy (>=2.0)
|
|
20
|
+
Requires-Dist: tigrbl-core
|
|
21
|
+
Requires-Dist: tigrbl-ops-oltp
|
|
22
|
+
Requires-Dist: tigrbl-typing
|
|
23
|
+
Requires-Dist: typing-extensions (>=4.0)
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
# tigrbl-atoms
|
|
29
|
+
|
|
30
|
+
    
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- Modular package in the Tigrbl namespace.
|
|
35
|
+
- Supports Python 3.10 through 3.12.
|
|
36
|
+
- Distributed as part of the swarmauri-sdk workspace.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### uv
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv add tigrbl-atoms
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### pip
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install tigrbl-atoms
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Import from the shared package-specific module namespaces after installation in your environment.
|
|
55
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# tigrbl-atoms
|
|
4
|
+
|
|
5
|
+
    
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Modular package in the Tigrbl namespace.
|
|
10
|
+
- Supports Python 3.10 through 3.12.
|
|
11
|
+
- Distributed as part of the swarmauri-sdk workspace.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### uv
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add tigrbl-atoms
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### pip
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install tigrbl-atoms
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Import from the shared package-specific module namespaces after installation in your environment.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tigrbl-atoms"
|
|
3
|
+
version = "0.1.0.dev5"
|
|
4
|
+
description = "Runtime atom utilities and execution helpers for Tigrbl."
|
|
5
|
+
license = "Apache-2.0"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
repository = "http://github.com/swarmauri/swarmauri-sdk"
|
|
8
|
+
requires-python = ">=3.10,<3.13"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"License :: OSI Approved :: Apache Software License",
|
|
11
|
+
"Development Status :: 1 - Planning",
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
]
|
|
19
|
+
authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"tigrbl-ops-oltp",
|
|
22
|
+
"tigrbl-core",
|
|
23
|
+
"tigrbl-typing",
|
|
24
|
+
"jinja2>=3.1",
|
|
25
|
+
"sqlalchemy>=2.0",
|
|
26
|
+
"typing-extensions>=4.0",
|
|
27
|
+
]
|
|
28
|
+
keywords = ["tigrbl", "sdk", "standards", "framework"]
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources]
|
|
31
|
+
"tigrbl-ops-oltp" = { workspace = true }
|
|
32
|
+
"tigrbl-core" = { workspace = true }
|
|
33
|
+
"tigrbl-typing" = { workspace = true }
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["poetry-core>=1.0.0"]
|
|
37
|
+
build-backend = "poetry.core.masonry.api"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
[tool.poetry]
|
|
41
|
+
packages = [
|
|
42
|
+
{ include = "tigrbl_atoms" },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"pytest>=8.0",
|
|
48
|
+
"pytest-asyncio>=0.24.0",
|
|
49
|
+
"pytest-timeout>=2.3.1",
|
|
50
|
+
"ruff>=0.9",
|
|
51
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .types import (
|
|
4
|
+
EGRESS_PHASES,
|
|
5
|
+
HANDLER_PHASES,
|
|
6
|
+
INGRESS_PHASES,
|
|
7
|
+
PHASE_SEQUENCE,
|
|
8
|
+
HookPhase,
|
|
9
|
+
HookPhases,
|
|
10
|
+
HookPredicate,
|
|
11
|
+
StepFn,
|
|
12
|
+
VALID_HOOK_PHASES,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"PHASE_SEQUENCE",
|
|
17
|
+
"INGRESS_PHASES",
|
|
18
|
+
"HANDLER_PHASES",
|
|
19
|
+
"EGRESS_PHASES",
|
|
20
|
+
"HookPhase",
|
|
21
|
+
"HookPhases",
|
|
22
|
+
"VALID_HOOK_PHASES",
|
|
23
|
+
"StepFn",
|
|
24
|
+
"HookPredicate",
|
|
25
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _ctx_view(ctx: Any) -> Dict[str, Any]:
|
|
7
|
+
"""Small read-only view for user callables."""
|
|
8
|
+
safe_view = getattr(ctx, "safe_view", None)
|
|
9
|
+
if callable(safe_view):
|
|
10
|
+
view = safe_view(include_temp=True)
|
|
11
|
+
return dict(view)
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
"op": getattr(ctx, "op", None),
|
|
15
|
+
"persist": getattr(ctx, "persist", True),
|
|
16
|
+
"temp": getattr(ctx, "temp", None),
|
|
17
|
+
"tenant": getattr(ctx, "tenant", None),
|
|
18
|
+
"user": getattr(ctx, "user", None),
|
|
19
|
+
"now": getattr(ctx, "now", None),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
__all__ = ["_ctx_view"]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _ensure_temp(ctx: Any) -> dict[str, Any]:
|
|
8
|
+
tmp = getattr(ctx, "temp", None)
|
|
9
|
+
if not isinstance(tmp, dict):
|
|
10
|
+
tmp = {}
|
|
11
|
+
setattr(ctx, "temp", tmp)
|
|
12
|
+
return tmp
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _ensure_ov(ctx: Any):
|
|
16
|
+
ov = getattr(ctx, "opview", None)
|
|
17
|
+
if ov is None:
|
|
18
|
+
raise RuntimeError("ctx_missing:opview")
|
|
19
|
+
return ov
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_schema_from_specs(ctx: Any) -> None:
|
|
23
|
+
specs = getattr(ctx, "specs", None)
|
|
24
|
+
op = getattr(ctx, "op", None)
|
|
25
|
+
if not isinstance(specs, Mapping) or not isinstance(op, str) or not op:
|
|
26
|
+
raise RuntimeError("ctx_missing:opview")
|
|
27
|
+
|
|
28
|
+
in_fields: list[str] = []
|
|
29
|
+
out_fields: list[str] = []
|
|
30
|
+
by_field_in: dict[str, dict[str, Any]] = {}
|
|
31
|
+
by_field_out: dict[str, dict[str, Any]] = {}
|
|
32
|
+
|
|
33
|
+
for field_name, spec in specs.items():
|
|
34
|
+
if not isinstance(field_name, str):
|
|
35
|
+
continue
|
|
36
|
+
io = getattr(spec, "io", None)
|
|
37
|
+
fs = getattr(spec, "field", None)
|
|
38
|
+
storage = getattr(spec, "storage", None)
|
|
39
|
+
|
|
40
|
+
in_verbs = set(getattr(io, "in_verbs", ()) or ())
|
|
41
|
+
out_verbs = set(getattr(io, "out_verbs", ()) or ())
|
|
42
|
+
|
|
43
|
+
if op in in_verbs:
|
|
44
|
+
in_fields.append(field_name)
|
|
45
|
+
in_meta: dict[str, Any] = {"in_enabled": True}
|
|
46
|
+
if storage is None:
|
|
47
|
+
in_meta["virtual"] = True
|
|
48
|
+
|
|
49
|
+
default_factory = getattr(spec, "default_factory", None)
|
|
50
|
+
if callable(default_factory):
|
|
51
|
+
in_meta["default_factory"] = default_factory
|
|
52
|
+
|
|
53
|
+
alias_in = getattr(io, "alias_in", None)
|
|
54
|
+
if alias_in:
|
|
55
|
+
in_meta["alias_in"] = alias_in
|
|
56
|
+
|
|
57
|
+
header_in = getattr(io, "header_in", None)
|
|
58
|
+
if header_in:
|
|
59
|
+
in_meta["header_in"] = header_in
|
|
60
|
+
in_meta["header_required_in"] = bool(
|
|
61
|
+
getattr(io, "header_required_in", False)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
in_meta["required"] = bool(fs and op in getattr(fs, "required_in", ()))
|
|
65
|
+
in_meta["nullable"] = (
|
|
66
|
+
True if storage is None else bool(getattr(storage, "nullable", True))
|
|
67
|
+
)
|
|
68
|
+
by_field_in[field_name] = in_meta
|
|
69
|
+
|
|
70
|
+
if op in out_verbs:
|
|
71
|
+
out_fields.append(field_name)
|
|
72
|
+
out_meta: dict[str, Any] = {}
|
|
73
|
+
|
|
74
|
+
alias_out = getattr(io, "alias_out", None)
|
|
75
|
+
if alias_out:
|
|
76
|
+
out_meta["alias_out"] = alias_out
|
|
77
|
+
if storage is None:
|
|
78
|
+
out_meta["virtual"] = True
|
|
79
|
+
|
|
80
|
+
py_type = getattr(getattr(fs, "py_type", None), "__name__", None)
|
|
81
|
+
if py_type:
|
|
82
|
+
out_meta["py_type"] = py_type
|
|
83
|
+
by_field_out[field_name] = out_meta
|
|
84
|
+
|
|
85
|
+
in_fields_sorted = tuple(sorted(in_fields))
|
|
86
|
+
out_fields_sorted = tuple(sorted(out_fields))
|
|
87
|
+
|
|
88
|
+
setattr(
|
|
89
|
+
ctx,
|
|
90
|
+
"schema_in",
|
|
91
|
+
{
|
|
92
|
+
"fields": in_fields_sorted,
|
|
93
|
+
"by_field": {f: by_field_in.get(f, {}) for f in in_fields_sorted},
|
|
94
|
+
"required": tuple(
|
|
95
|
+
f for f in in_fields_sorted if by_field_in.get(f, {}).get("required")
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
setattr(
|
|
100
|
+
ctx,
|
|
101
|
+
"schema_out",
|
|
102
|
+
{
|
|
103
|
+
"fields": out_fields_sorted,
|
|
104
|
+
"by_field": {f: by_field_out.get(f, {}) for f in out_fields_sorted},
|
|
105
|
+
"expose": out_fields_sorted,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _ensure_schema_in(ctx: Any) -> Mapping[str, Any]:
|
|
111
|
+
temp = _ensure_temp(ctx)
|
|
112
|
+
cached = temp.get("schema_in")
|
|
113
|
+
if isinstance(cached, Mapping):
|
|
114
|
+
return cached
|
|
115
|
+
|
|
116
|
+
schema_in = getattr(ctx, "schema_in", None)
|
|
117
|
+
if isinstance(schema_in, Mapping):
|
|
118
|
+
temp["schema_in"] = schema_in
|
|
119
|
+
return schema_in
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
ov = _ensure_ov(ctx)
|
|
123
|
+
bf = ov.schema_in.by_field
|
|
124
|
+
req = tuple(n for n, e in bf.items() if e.get("required"))
|
|
125
|
+
temp["schema_in"] = {
|
|
126
|
+
"fields": ov.schema_in.fields,
|
|
127
|
+
"by_field": bf,
|
|
128
|
+
"required": req,
|
|
129
|
+
}
|
|
130
|
+
except RuntimeError as exc:
|
|
131
|
+
if str(exc) != "ctx_missing:opview":
|
|
132
|
+
raise
|
|
133
|
+
_normalize_schema_from_specs(ctx)
|
|
134
|
+
temp["schema_in"] = getattr(ctx, "schema_in")
|
|
135
|
+
return temp["schema_in"]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _ensure_schema_out(ctx: Any) -> Mapping[str, Any]:
|
|
139
|
+
temp = _ensure_temp(ctx)
|
|
140
|
+
cached = temp.get("schema_out")
|
|
141
|
+
if isinstance(cached, Mapping):
|
|
142
|
+
return cached
|
|
143
|
+
|
|
144
|
+
schema_out = getattr(ctx, "schema_out", None)
|
|
145
|
+
if isinstance(schema_out, Mapping):
|
|
146
|
+
temp["schema_out"] = schema_out
|
|
147
|
+
return schema_out
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
ov = _ensure_ov(ctx)
|
|
151
|
+
temp["schema_out"] = {
|
|
152
|
+
"fields": ov.schema_out.fields,
|
|
153
|
+
"by_field": ov.schema_out.by_field,
|
|
154
|
+
"expose": ov.schema_out.expose,
|
|
155
|
+
}
|
|
156
|
+
except RuntimeError as exc:
|
|
157
|
+
if str(exc) != "ctx_missing:opview":
|
|
158
|
+
raise
|
|
159
|
+
_normalize_schema_from_specs(ctx)
|
|
160
|
+
temp["schema_out"] = getattr(ctx, "schema_out")
|
|
161
|
+
return temp["schema_out"]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as json_module
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from http.cookies import SimpleCookie
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import Any
|
|
8
|
+
from collections.abc import Iterable, Mapping
|
|
9
|
+
from urllib.parse import parse_qs
|
|
10
|
+
|
|
11
|
+
from tigrbl_core._spec.request_spec import RequestSpec
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
from tigrbl_typing.request import AwaitableValue, URL
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(init=False)
|
|
18
|
+
class Request(RequestSpec):
|
|
19
|
+
method: str
|
|
20
|
+
path: str
|
|
21
|
+
headers: Mapping[str, str] | Iterable[tuple[str, str]]
|
|
22
|
+
query: dict[str, list[str]]
|
|
23
|
+
path_params: dict[str, str]
|
|
24
|
+
body: bytes
|
|
25
|
+
script_name: str = ""
|
|
26
|
+
app: Any | None = None
|
|
27
|
+
state: SimpleNamespace = field(default_factory=SimpleNamespace)
|
|
28
|
+
scope: dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
_json_cache: Any = field(default=None, init=False, repr=False)
|
|
30
|
+
_json_loaded: bool = field(default=False, init=False, repr=False)
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
method: str | dict[str, Any],
|
|
35
|
+
path: str | None = None,
|
|
36
|
+
headers: Mapping[str, str] | Iterable[tuple[str, str]] | None = None,
|
|
37
|
+
query: dict[str, list[str]] | None = None,
|
|
38
|
+
path_params: dict[str, str] | None = None,
|
|
39
|
+
body: bytes = b"",
|
|
40
|
+
script_name: str = "",
|
|
41
|
+
app: Any | None = None,
|
|
42
|
+
state: SimpleNamespace | None = None,
|
|
43
|
+
scope: dict[str, Any] | None = None,
|
|
44
|
+
receive: Any | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
del receive
|
|
47
|
+
|
|
48
|
+
self._json_cache = None
|
|
49
|
+
self._json_loaded = False
|
|
50
|
+
|
|
51
|
+
if isinstance(method, dict):
|
|
52
|
+
if scope is not None:
|
|
53
|
+
raise TypeError("scope cannot be provided when first argument is scope")
|
|
54
|
+
self._init_from_scope(method, app=app, state=state)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if path is None:
|
|
58
|
+
raise TypeError("path is required when constructing Request from fields")
|
|
59
|
+
|
|
60
|
+
self.method = method
|
|
61
|
+
self.path = path
|
|
62
|
+
self.headers = headers or {}
|
|
63
|
+
self.query = query or {}
|
|
64
|
+
self.path_params = path_params or {}
|
|
65
|
+
self.body = body
|
|
66
|
+
self.script_name = script_name
|
|
67
|
+
self.app = app
|
|
68
|
+
self.state = state or SimpleNamespace()
|
|
69
|
+
self.scope = scope or {}
|
|
70
|
+
|
|
71
|
+
def _init_from_scope(
|
|
72
|
+
self,
|
|
73
|
+
scope: dict[str, Any],
|
|
74
|
+
*,
|
|
75
|
+
app: Any | None,
|
|
76
|
+
state: SimpleNamespace | None,
|
|
77
|
+
) -> None:
|
|
78
|
+
self.method = (scope.get("method") or "GET").upper()
|
|
79
|
+
self.path = scope.get("path") or "/"
|
|
80
|
+
self.headers = {
|
|
81
|
+
key.decode("latin-1").lower(): value.decode("latin-1")
|
|
82
|
+
for key, value in scope.get("headers", [])
|
|
83
|
+
}
|
|
84
|
+
self.query = parse_qs(
|
|
85
|
+
scope.get("query_string", b"").decode("latin-1"), keep_blank_values=True
|
|
86
|
+
)
|
|
87
|
+
self.path_params = scope.get("path_params") or {}
|
|
88
|
+
self.body = b""
|
|
89
|
+
self.script_name = scope.get("root_path") or ""
|
|
90
|
+
self.app = app
|
|
91
|
+
self.state = state or SimpleNamespace()
|
|
92
|
+
self.scope = scope
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_scope(
|
|
96
|
+
cls,
|
|
97
|
+
scope: dict[str, Any],
|
|
98
|
+
receive: Any | None = None,
|
|
99
|
+
*,
|
|
100
|
+
app: Any | None = None,
|
|
101
|
+
state: SimpleNamespace | None = None,
|
|
102
|
+
) -> "Request":
|
|
103
|
+
return cls(scope, app=app, state=state, receive=receive)
|
|
104
|
+
|
|
105
|
+
def json(self) -> AwaitableValue:
|
|
106
|
+
return AwaitableValue(self.json_sync())
|
|
107
|
+
|
|
108
|
+
def json_sync(self) -> Any:
|
|
109
|
+
if self._json_loaded:
|
|
110
|
+
return self._json_cache
|
|
111
|
+
if not self.body:
|
|
112
|
+
self._json_loaded = True
|
|
113
|
+
self._json_cache = None
|
|
114
|
+
return None
|
|
115
|
+
self._json_cache = json_module.loads(self.body.decode("utf-8"))
|
|
116
|
+
self._json_loaded = True
|
|
117
|
+
return self._json_cache
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def url(self) -> URL:
|
|
121
|
+
return URL(path=self.path, query=self.query, script_name=self.script_name)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def query_params(self) -> dict[str, str]:
|
|
125
|
+
return {name: vals[0] for name, vals in self.query.items() if vals}
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def cookies(self) -> dict[str, str]:
|
|
129
|
+
raw = self.headers.get("cookie", "") or ""
|
|
130
|
+
parsed = SimpleCookie()
|
|
131
|
+
parsed.load(raw)
|
|
132
|
+
return {name: morsel.value for name, morsel in parsed.items()}
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def bearer_token(self) -> str | None:
|
|
136
|
+
authorization = self.headers.get("authorization", "") or ""
|
|
137
|
+
scheme, _, token = authorization.partition(" ")
|
|
138
|
+
cleaned = token.strip()
|
|
139
|
+
return cleaned if scheme.lower() == "bearer" and cleaned else None
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def admin_key(self) -> str | None:
|
|
143
|
+
key = (self.headers.get("x-admin-key") or "").strip()
|
|
144
|
+
return key or None
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def session_token(self) -> str | None:
|
|
148
|
+
return self.bearer_token or (self.cookies.get("sid") or "").strip() or None
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def client(self) -> SimpleNamespace:
|
|
152
|
+
host = ""
|
|
153
|
+
client = self.scope.get("client") if isinstance(self.scope, dict) else None
|
|
154
|
+
if isinstance(client, tuple) and client:
|
|
155
|
+
host = str(client[0])
|
|
156
|
+
return SimpleNamespace(ip=host, host=host)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = ["Request", "URL", "AwaitableValue"]
|