modularizer-plat-browser 0.17.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.
@@ -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,11 @@
1
+ plat_browser/__init__.py,sha256=TkMVt7XSNyP8E-L2Dw2BIUEtlheyaNXV4ZfavBgAfOs,1002
2
+ plat_browser/client.py,sha256=Mg2lXlKF79vceKM2R2SX4aGq4TZdI0Z2X6OvMxhM98U,4200
3
+ plat_browser/decorators.py,sha256=GWrvPXKnseWLmIL-wA1yZMIsHjOFzr-hM6mPVHMSDgE,2826
4
+ plat_browser/errors.py,sha256=WNeJBHYdIGI1QRmasiuxNoE5h77fZA6CefPDuC29BRA,787
5
+ plat_browser/metadata.py,sha256=ahrkAY-IhUQpNyNN3iT_cWELA5bjazEfaokiZZRYxbU,472
6
+ plat_browser/plugins.py,sha256=y09hxAsZd98B1vAXS2EuukBP0n9vAqNeXPDvNef0Gjo,16621
7
+ plat_browser/server.py,sha256=ywUfaMELUMax5pxsJIWthCsf5IWuppugS8hOMt0F-G0,36708
8
+ plat_browser/types.py,sha256=1CW1x0x_xtcggkW_StZisWV6d7nrXLuJigjBOh64sh8,1209
9
+ modularizer_plat_browser-0.17.1.dist-info/METADATA,sha256=9-GkxQwe-_92WavsvTqDhpIMePeBvdhRi0MlVrpANX8,2786
10
+ modularizer_plat_browser-0.17.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
11
+ modularizer_plat_browser-0.17.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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
+ ]
plat_browser/client.py ADDED
@@ -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")
plat_browser/errors.py ADDED
@@ -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