modularizer-plat-browser 0.17.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.
- modularizer_plat_browser-0.17.1/.gitignore +92 -0
- modularizer_plat_browser-0.17.1/PKG-INFO +99 -0
- modularizer_plat_browser-0.17.1/README.md +91 -0
- modularizer_plat_browser-0.17.1/plat_browser/__init__.py +40 -0
- modularizer_plat_browser-0.17.1/plat_browser/client.py +129 -0
- modularizer_plat_browser-0.17.1/plat_browser/decorators.py +93 -0
- modularizer_plat_browser-0.17.1/plat_browser/errors.py +30 -0
- modularizer_plat_browser-0.17.1/plat_browser/metadata.py +18 -0
- modularizer_plat_browser-0.17.1/plat_browser/plugins.py +523 -0
- modularizer_plat_browser-0.17.1/plat_browser/server.py +884 -0
- modularizer_plat_browser-0.17.1/plat_browser/types.py +50 -0
- modularizer_plat_browser-0.17.1/pyproject.toml +14 -0
- modularizer_plat_browser-0.17.1/tests/test_plat_browser.py +416 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
############################
|
|
2
|
+
# Node
|
|
3
|
+
############################
|
|
4
|
+
|
|
5
|
+
node_modules/
|
|
6
|
+
npm-debug.log*
|
|
7
|
+
yarn-debug.log*
|
|
8
|
+
yarn-error.log*
|
|
9
|
+
pnpm-debug.log*
|
|
10
|
+
|
|
11
|
+
############################
|
|
12
|
+
# Build output
|
|
13
|
+
############################
|
|
14
|
+
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
coverage/
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
############################
|
|
22
|
+
# TypeScript
|
|
23
|
+
############################
|
|
24
|
+
|
|
25
|
+
*.tsbuildinfo
|
|
26
|
+
|
|
27
|
+
############################
|
|
28
|
+
# Environment variables
|
|
29
|
+
############################
|
|
30
|
+
|
|
31
|
+
.env
|
|
32
|
+
typpescript/.env.sample
|
|
33
|
+
!.env.example
|
|
34
|
+
|
|
35
|
+
############################
|
|
36
|
+
# Logs
|
|
37
|
+
############################
|
|
38
|
+
|
|
39
|
+
logs/
|
|
40
|
+
*.log
|
|
41
|
+
|
|
42
|
+
############################
|
|
43
|
+
# Editors
|
|
44
|
+
############################
|
|
45
|
+
|
|
46
|
+
.vscode/
|
|
47
|
+
.idea/
|
|
48
|
+
*.swp
|
|
49
|
+
*.swo
|
|
50
|
+
*.swn
|
|
51
|
+
*~
|
|
52
|
+
|
|
53
|
+
############################
|
|
54
|
+
# OS files
|
|
55
|
+
############################
|
|
56
|
+
|
|
57
|
+
.DS_Store
|
|
58
|
+
Thumbs.db
|
|
59
|
+
|
|
60
|
+
############################
|
|
61
|
+
# Temporary files
|
|
62
|
+
############################
|
|
63
|
+
|
|
64
|
+
tmp/
|
|
65
|
+
temp/
|
|
66
|
+
|
|
67
|
+
############################
|
|
68
|
+
# Misc
|
|
69
|
+
############################
|
|
70
|
+
|
|
71
|
+
*.pid
|
|
72
|
+
*.seed
|
|
73
|
+
*.lock
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
###
|
|
78
|
+
**/*u-*
|
|
79
|
+
*-u.*
|
|
80
|
+
**/*untracked-*
|
|
81
|
+
*-untracked.*
|
|
82
|
+
**/*u_*
|
|
83
|
+
*_u.*
|
|
84
|
+
**/*untracked_*
|
|
85
|
+
*_untracked.*
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
*venv*
|
|
89
|
+
*.egg-info
|
|
90
|
+
__pycache__/
|
|
91
|
+
*.py[cod]
|
|
92
|
+
.pytest_cache/
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: modularizer-plat-browser
|
|
3
|
+
Version: 0.17.1
|
|
4
|
+
Summary: Browser-hosted Python runtime for plat
|
|
5
|
+
License-Expression: Unlicense
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
|
|
9
|
+
# plat_browser
|
|
10
|
+
|
|
11
|
+
Browser-hosted Python runtime for plat.
|
|
12
|
+
|
|
13
|
+
This package is intended for client-side Python execution environments while keeping the main `plat` package focused on normal server/client/CLI usage.
|
|
14
|
+
|
|
15
|
+
## Example
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from plat_browser import Controller, POST, serve_server
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@Controller()
|
|
22
|
+
class MathApi:
|
|
23
|
+
@POST()
|
|
24
|
+
def add(self, a: int, b: int, ctx):
|
|
25
|
+
return a + b
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
serve_server("browser-python-math", [MathApi])
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
It also supports hidden browser package installation syntax:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
!pip install pandas
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The browser host/runtime is responsible for package loading and transport bridging. User code should stay normal Python.
|
|
38
|
+
|
|
39
|
+
Payload fields are passed into handlers as keyword arguments by default, so browser Python code can use normal Python signatures like `def add(self, a, b, ctx)`.
|
|
40
|
+
|
|
41
|
+
Browser Python can also act as a client:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from plat_browser import connect_server
|
|
45
|
+
|
|
46
|
+
client = await connect_server("css://browser-python-math")
|
|
47
|
+
await client.add(a=20, b=22)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Browser Python clients can also pass optional trust inputs:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
client = await connect_server(
|
|
54
|
+
"css://browser-python-math",
|
|
55
|
+
{
|
|
56
|
+
"identity": {
|
|
57
|
+
"known_hosts": {},
|
|
58
|
+
"authority_servers": [
|
|
59
|
+
{
|
|
60
|
+
"base_url": "https://authority.example.com",
|
|
61
|
+
"public_key_jwk": {...},
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That same client helper can also connect to normal HTTP servers that expose OpenAPI:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from plat_browser import connect_server
|
|
73
|
+
|
|
74
|
+
client = await connect_server("https://api.example.com")
|
|
75
|
+
await client.createOrder(itemId="sku_123", qty=2)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## css:// host identity
|
|
79
|
+
|
|
80
|
+
The `css://` transport can keep a stable host identity too.
|
|
81
|
+
|
|
82
|
+
- Browser-hosted servers can reuse a persisted public/private keypair
|
|
83
|
+
- Clients can trust on first use and remember a host key for later
|
|
84
|
+
- Clients can also verify a signed authority record for well-known server names
|
|
85
|
+
|
|
86
|
+
Those identity helpers live in the TypeScript/browser host layer so browser Python user code can stay normal Python and never mention lower-level runtime details.
|
|
87
|
+
|
|
88
|
+
## TypeScript host utilities
|
|
89
|
+
|
|
90
|
+
Use the browser host utilities from the TypeScript package:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { startClientSidePythonServerFromSource } from '@modularizer/plat/python-browser'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That host layer is responsible for:
|
|
97
|
+
- loading the hidden browser Python runtime
|
|
98
|
+
- handling `!pip install ...` and import-based package installation
|
|
99
|
+
- serving the Python-defined methods over the same client-side-server transport utilities
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# plat_browser
|
|
2
|
+
|
|
3
|
+
Browser-hosted Python runtime for plat.
|
|
4
|
+
|
|
5
|
+
This package is intended for client-side Python execution environments while keeping the main `plat` package focused on normal server/client/CLI usage.
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from plat_browser import Controller, POST, serve_server
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@Controller()
|
|
14
|
+
class MathApi:
|
|
15
|
+
@POST()
|
|
16
|
+
def add(self, a: int, b: int, ctx):
|
|
17
|
+
return a + b
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
serve_server("browser-python-math", [MathApi])
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
It also supports hidden browser package installation syntax:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
!pip install pandas
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The browser host/runtime is responsible for package loading and transport bridging. User code should stay normal Python.
|
|
30
|
+
|
|
31
|
+
Payload fields are passed into handlers as keyword arguments by default, so browser Python code can use normal Python signatures like `def add(self, a, b, ctx)`.
|
|
32
|
+
|
|
33
|
+
Browser Python can also act as a client:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from plat_browser import connect_server
|
|
37
|
+
|
|
38
|
+
client = await connect_server("css://browser-python-math")
|
|
39
|
+
await client.add(a=20, b=22)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Browser Python clients can also pass optional trust inputs:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
client = await connect_server(
|
|
46
|
+
"css://browser-python-math",
|
|
47
|
+
{
|
|
48
|
+
"identity": {
|
|
49
|
+
"known_hosts": {},
|
|
50
|
+
"authority_servers": [
|
|
51
|
+
{
|
|
52
|
+
"base_url": "https://authority.example.com",
|
|
53
|
+
"public_key_jwk": {...},
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
That same client helper can also connect to normal HTTP servers that expose OpenAPI:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from plat_browser import connect_server
|
|
65
|
+
|
|
66
|
+
client = await connect_server("https://api.example.com")
|
|
67
|
+
await client.createOrder(itemId="sku_123", qty=2)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## css:// host identity
|
|
71
|
+
|
|
72
|
+
The `css://` transport can keep a stable host identity too.
|
|
73
|
+
|
|
74
|
+
- Browser-hosted servers can reuse a persisted public/private keypair
|
|
75
|
+
- Clients can trust on first use and remember a host key for later
|
|
76
|
+
- Clients can also verify a signed authority record for well-known server names
|
|
77
|
+
|
|
78
|
+
Those identity helpers live in the TypeScript/browser host layer so browser Python user code can stay normal Python and never mention lower-level runtime details.
|
|
79
|
+
|
|
80
|
+
## TypeScript host utilities
|
|
81
|
+
|
|
82
|
+
Use the browser host utilities from the TypeScript package:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { startClientSidePythonServerFromSource } from '@modularizer/plat/python-browser'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That host layer is responsible for:
|
|
89
|
+
- loading the hidden browser Python runtime
|
|
90
|
+
- handling `!pip install ...` and import-based package installation
|
|
91
|
+
- serving the Python-defined methods over the same client-side-server transport utilities
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .client import BrowserPLATClient, connect_client_side_server, connect_server, run_python_client_source
|
|
2
|
+
from .decorators import Controller, DELETE, GET, PATCH, POST, PUT
|
|
3
|
+
from .errors import HttpError, HttpResponse
|
|
4
|
+
from .plugins import BucketConfig
|
|
5
|
+
from .server import (
|
|
6
|
+
BrowserPackagePlan,
|
|
7
|
+
BrowserPLATServer,
|
|
8
|
+
BrowserServerDefinition,
|
|
9
|
+
create_browser_server,
|
|
10
|
+
create_server,
|
|
11
|
+
prepare_python_source,
|
|
12
|
+
serve_server,
|
|
13
|
+
serve_client_side_server,
|
|
14
|
+
)
|
|
15
|
+
from .types import RouteContext
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Controller",
|
|
19
|
+
"DELETE",
|
|
20
|
+
"GET",
|
|
21
|
+
"PATCH",
|
|
22
|
+
"POST",
|
|
23
|
+
"PUT",
|
|
24
|
+
"BrowserPLATClient",
|
|
25
|
+
"BrowserPackagePlan",
|
|
26
|
+
"BrowserPLATServer",
|
|
27
|
+
"BrowserServerDefinition",
|
|
28
|
+
"BucketConfig",
|
|
29
|
+
"HttpError",
|
|
30
|
+
"HttpResponse",
|
|
31
|
+
"RouteContext",
|
|
32
|
+
"connect_client_side_server",
|
|
33
|
+
"connect_server",
|
|
34
|
+
"create_browser_server",
|
|
35
|
+
"create_server",
|
|
36
|
+
"prepare_python_source",
|
|
37
|
+
"run_python_client_source",
|
|
38
|
+
"serve_server",
|
|
39
|
+
"serve_client_side_server",
|
|
40
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BrowserClientConnectBridge = Callable[[str, Any], Awaitable[Any]]
|
|
9
|
+
BrowserClientCallBridge = Callable[[int, str, Any], Awaitable[Any]]
|
|
10
|
+
|
|
11
|
+
_CONNECT_BRIDGE: BrowserClientConnectBridge | None = None
|
|
12
|
+
_CALL_BRIDGE: BrowserClientCallBridge | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BrowserPLATClient:
|
|
16
|
+
def __init__(self, client_id: int, base_url: str, openapi: Any = None) -> None:
|
|
17
|
+
self.client_id = client_id
|
|
18
|
+
self.base_url = base_url
|
|
19
|
+
self.openapi = openapi
|
|
20
|
+
|
|
21
|
+
async def call(self, method_name: str, input: Any = None, /, **kwargs: Any) -> Any:
|
|
22
|
+
call_bridge = _require_call_bridge()
|
|
23
|
+
payload = _merge_input(input, kwargs)
|
|
24
|
+
result = await call_bridge(self.client_id, method_name, payload)
|
|
25
|
+
return _to_python_value(result)
|
|
26
|
+
|
|
27
|
+
def __getattr__(self, method_name: str):
|
|
28
|
+
async def invoke(input: Any = None, /, **kwargs: Any) -> Any:
|
|
29
|
+
return await self.call(method_name, input, **kwargs)
|
|
30
|
+
|
|
31
|
+
return invoke
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _set_browser_client_bridge(
|
|
35
|
+
connect_bridge: BrowserClientConnectBridge,
|
|
36
|
+
call_bridge: BrowserClientCallBridge,
|
|
37
|
+
) -> None:
|
|
38
|
+
global _CONNECT_BRIDGE, _CALL_BRIDGE
|
|
39
|
+
_CONNECT_BRIDGE = connect_bridge
|
|
40
|
+
_CALL_BRIDGE = call_bridge
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def connect_client_side_server(base_url: str, options: Any = None) -> BrowserPLATClient:
|
|
44
|
+
connect_bridge = _require_connect_bridge()
|
|
45
|
+
connection = _to_python_value(await connect_bridge(base_url, options))
|
|
46
|
+
if not isinstance(connection, dict):
|
|
47
|
+
raise RuntimeError("Browser client bridge returned an invalid connection payload.")
|
|
48
|
+
return BrowserPLATClient(
|
|
49
|
+
client_id=int(connection["client_id"]),
|
|
50
|
+
base_url=str(connection.get("base_url") or base_url),
|
|
51
|
+
openapi=connection.get("openapi"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Server-style parity alias.
|
|
56
|
+
connect_server = connect_client_side_server
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def run_python_client_source(source: str) -> Any:
|
|
60
|
+
namespace = {
|
|
61
|
+
"__name__": "__plat_browser_client__",
|
|
62
|
+
"connect_client_side_server": connect_client_side_server,
|
|
63
|
+
}
|
|
64
|
+
compiled = _compile_python_client_snippet(source)
|
|
65
|
+
exec(compiled, namespace)
|
|
66
|
+
result = await namespace["__plat_browser_client_main__"]()
|
|
67
|
+
return _to_python_value(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _compile_python_client_snippet(source: str):
|
|
71
|
+
module = ast.parse(source, mode="exec")
|
|
72
|
+
body = list(module.body)
|
|
73
|
+
if body and isinstance(body[-1], ast.Expr):
|
|
74
|
+
body[-1] = ast.Return(body[-1].value)
|
|
75
|
+
else:
|
|
76
|
+
body.append(ast.Return(value=ast.Constant(value=None)))
|
|
77
|
+
function = ast.AsyncFunctionDef(
|
|
78
|
+
name="__plat_browser_client_main__",
|
|
79
|
+
args=ast.arguments(
|
|
80
|
+
posonlyargs=[],
|
|
81
|
+
args=[],
|
|
82
|
+
kwonlyargs=[],
|
|
83
|
+
kw_defaults=[],
|
|
84
|
+
defaults=[],
|
|
85
|
+
vararg=None,
|
|
86
|
+
kwarg=None,
|
|
87
|
+
),
|
|
88
|
+
body=body,
|
|
89
|
+
decorator_list=[],
|
|
90
|
+
returns=None,
|
|
91
|
+
type_comment=None,
|
|
92
|
+
)
|
|
93
|
+
wrapper = ast.Module(body=[function], type_ignores=[])
|
|
94
|
+
ast.fix_missing_locations(wrapper)
|
|
95
|
+
return compile(wrapper, "<plat_browser_client>", "exec")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _merge_input(input_data: Any, kwargs: dict[str, Any]) -> Any:
|
|
99
|
+
if input_data is None:
|
|
100
|
+
return kwargs or {}
|
|
101
|
+
if kwargs:
|
|
102
|
+
if isinstance(input_data, dict):
|
|
103
|
+
merged = dict(input_data)
|
|
104
|
+
merged.update(kwargs)
|
|
105
|
+
return merged
|
|
106
|
+
raise TypeError("Cannot pass both a non-dict positional input and keyword arguments.")
|
|
107
|
+
return input_data
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _to_python_value(value: Any) -> Any:
|
|
111
|
+
to_py = getattr(value, "to_py", None)
|
|
112
|
+
if callable(to_py):
|
|
113
|
+
try:
|
|
114
|
+
return to_py()
|
|
115
|
+
except TypeError:
|
|
116
|
+
return to_py(depth=-1)
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _require_connect_bridge() -> BrowserClientConnectBridge:
|
|
121
|
+
if _CONNECT_BRIDGE is None:
|
|
122
|
+
raise RuntimeError("Browser Python client bridge is not installed in this runtime.")
|
|
123
|
+
return _CONNECT_BRIDGE
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _require_call_bridge() -> BrowserClientCallBridge:
|
|
127
|
+
if _CALL_BRIDGE is None:
|
|
128
|
+
raise RuntimeError("Browser Python client bridge is not installed in this runtime.")
|
|
129
|
+
return _CALL_BRIDGE
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping
|
|
4
|
+
from typing import Any, TypeVar, cast
|
|
5
|
+
|
|
6
|
+
from .metadata import ensure_controller_meta
|
|
7
|
+
from .types import ControllerMeta, HttpMethod, RouteMeta
|
|
8
|
+
|
|
9
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
|
+
ROUTE_METADATA_KEY = "__plat_browser_route_meta__"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def Controller(
|
|
14
|
+
controller_name: str | None = None,
|
|
15
|
+
opts: Mapping[str, Any] | None = None,
|
|
16
|
+
):
|
|
17
|
+
options = dict(opts or {})
|
|
18
|
+
|
|
19
|
+
def decorator(cls: type) -> type:
|
|
20
|
+
meta = ensure_controller_meta(cls)
|
|
21
|
+
_apply_controller_options(meta, cls, controller_name, options)
|
|
22
|
+
return cls
|
|
23
|
+
|
|
24
|
+
return decorator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _apply_controller_options(
|
|
28
|
+
meta: ControllerMeta,
|
|
29
|
+
cls: type,
|
|
30
|
+
controller_name: str | None,
|
|
31
|
+
options: dict[str, Any],
|
|
32
|
+
) -> None:
|
|
33
|
+
meta.base_path = controller_name or cls.__name__
|
|
34
|
+
meta.tag = cast(str | None, options.get("tag")) or controller_name or cls.__name__
|
|
35
|
+
meta.auth = cast(str | None, options.get("auth"))
|
|
36
|
+
meta.rate_limit = options.get("rateLimit", options.get("rate_limit"))
|
|
37
|
+
meta.token_limit = options.get("tokenLimit", options.get("token_limit"))
|
|
38
|
+
meta.cache = options.get("cache")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _route_decorator(method: HttpMethod):
|
|
42
|
+
def factory(
|
|
43
|
+
arg: str | Mapping[str, Any] | F | None = None,
|
|
44
|
+
opts: Mapping[str, Any] | None = None,
|
|
45
|
+
):
|
|
46
|
+
if callable(arg):
|
|
47
|
+
return _decorate_route(cast(F, arg), method, None, {})
|
|
48
|
+
|
|
49
|
+
decorator_path: str | None = None
|
|
50
|
+
options: dict[str, Any] = dict(opts or {})
|
|
51
|
+
if isinstance(arg, str):
|
|
52
|
+
decorator_path = arg
|
|
53
|
+
elif isinstance(arg, Mapping):
|
|
54
|
+
options = dict(arg)
|
|
55
|
+
elif arg is not None:
|
|
56
|
+
raise TypeError(f"Unsupported decorator argument for {method}: {arg!r}")
|
|
57
|
+
|
|
58
|
+
def decorator(fn: F) -> F:
|
|
59
|
+
return _decorate_route(fn, method, decorator_path, options)
|
|
60
|
+
|
|
61
|
+
return decorator
|
|
62
|
+
|
|
63
|
+
return factory
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _decorate_route(
|
|
67
|
+
fn: F,
|
|
68
|
+
method: HttpMethod,
|
|
69
|
+
decorator_path: str | None,
|
|
70
|
+
options: dict[str, Any],
|
|
71
|
+
) -> F:
|
|
72
|
+
meta = RouteMeta(
|
|
73
|
+
name=fn.__name__,
|
|
74
|
+
method=method,
|
|
75
|
+
path=f"/{fn.__name__}",
|
|
76
|
+
decorator_path=decorator_path,
|
|
77
|
+
auth=cast(str | None, options.get("auth")),
|
|
78
|
+
summary=cast(str | None, options.get("summary")),
|
|
79
|
+
description=cast(str | None, options.get("description")),
|
|
80
|
+
rate_limit=options.get("rateLimit", options.get("rate_limit")),
|
|
81
|
+
token_limit=options.get("tokenLimit", options.get("token_limit")),
|
|
82
|
+
cache=options.get("cache"),
|
|
83
|
+
opts=options or None,
|
|
84
|
+
)
|
|
85
|
+
fn.__dict__[ROUTE_METADATA_KEY] = meta
|
|
86
|
+
return fn
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
GET = _route_decorator("GET")
|
|
90
|
+
POST = _route_decorator("POST")
|
|
91
|
+
PUT = _route_decorator("PUT")
|
|
92
|
+
PATCH = _route_decorator("PATCH")
|
|
93
|
+
DELETE = _route_decorator("DELETE")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class HttpResponse(Exception):
|
|
9
|
+
status_code: int
|
|
10
|
+
body: Any = None
|
|
11
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
12
|
+
|
|
13
|
+
def __post_init__(self) -> None:
|
|
14
|
+
super().__init__(f"HTTP {self.status_code}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HttpError(HttpResponse):
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
status_code: int,
|
|
21
|
+
message: str,
|
|
22
|
+
data: Any = None,
|
|
23
|
+
headers: dict[str, str] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
body: dict[str, Any] = {"error": message}
|
|
26
|
+
if data is not None:
|
|
27
|
+
body["data"] = data
|
|
28
|
+
super().__init__(status_code=status_code, body=body, headers=headers or {})
|
|
29
|
+
self.message = message
|
|
30
|
+
self.data = data
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .types import ControllerMeta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
CONTROLLER_METADATA_KEY = "__plat_browser_controller_meta__"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_controller_meta(ctor: type) -> ControllerMeta | None:
|
|
10
|
+
return getattr(ctor, CONTROLLER_METADATA_KEY, None)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ensure_controller_meta(ctor: type) -> ControllerMeta:
|
|
14
|
+
meta = get_controller_meta(ctor)
|
|
15
|
+
if meta is None:
|
|
16
|
+
meta = ControllerMeta()
|
|
17
|
+
setattr(ctor, CONTROLLER_METADATA_KEY, meta)
|
|
18
|
+
return meta
|