crosswind-workers 0.1.0__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.
- _crosswind_patches/__init__.py +13 -0
- _crosswind_patches/entropy.py +259 -0
- _crosswind_patches/runtime.py +45 -0
- asgi/__init__.py +7 -0
- crosswind_workers-0.1.0.data/purelib/crosswind_workers_patches.pth +1 -0
- crosswind_workers-0.1.0.dist-info/METADATA +9 -0
- crosswind_workers-0.1.0.dist-info/RECORD +35 -0
- crosswind_workers-0.1.0.dist-info/WHEEL +4 -0
- workers/__init__.py +88 -0
- workers/_adapter.py +24 -0
- workers/_body.py +34 -0
- workers/_js.py +121 -0
- workers/asgi.py +523 -0
- workers/bindings/__init__.py +36 -0
- workers/bindings/base.py +135 -0
- workers/bindings/d1.py +47 -0
- workers/bindings/env.py +50 -0
- workers/bindings/kv.py +121 -0
- workers/bindings/queues.py +36 -0
- workers/bindings/r2.py +130 -0
- workers/bindings/services.py +55 -0
- workers/bindings/workflow.py +64 -0
- workers/blob.py +158 -0
- workers/context.py +230 -0
- workers/entrypoints.py +183 -0
- workers/fetch.py +51 -0
- workers/formdata.py +92 -0
- workers/py.typed +0 -0
- workers/request.py +138 -0
- workers/response.py +181 -0
- workers/rpc.py +154 -0
- workers/types.py +57 -0
- workers/utils.py +55 -0
- workers/workflows.py +265 -0
- workers/wsgi.py +426 -0
workers/bindings/d1.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..rpc import python_from_rpc, python_to_rpc
|
|
5
|
+
from .base import Binding, _js_options
|
|
6
|
+
|
|
7
|
+
__all__ = ["D1Database", "D1PreparedStatement"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class D1PreparedStatement(Binding):
|
|
11
|
+
def bind(self, *values: Any) -> "D1PreparedStatement":
|
|
12
|
+
return D1PreparedStatement(self.js_object.bind(*map(python_to_rpc, values)))
|
|
13
|
+
|
|
14
|
+
async def first(self, column: str | None = None) -> Any:
|
|
15
|
+
args = () if column is None else (column,)
|
|
16
|
+
return python_from_rpc(await self.js_object.first(*args))
|
|
17
|
+
|
|
18
|
+
async def all(self) -> dict[str, Any]:
|
|
19
|
+
return python_from_rpc(await self.js_object.all())
|
|
20
|
+
|
|
21
|
+
async def run(self) -> dict[str, Any]:
|
|
22
|
+
return python_from_rpc(await self.js_object.run())
|
|
23
|
+
|
|
24
|
+
async def raw(
|
|
25
|
+
self,
|
|
26
|
+
options: Any = None,
|
|
27
|
+
/,
|
|
28
|
+
*,
|
|
29
|
+
column_names: bool | None = None,
|
|
30
|
+
**extra: Any,
|
|
31
|
+
) -> list[list[Any]]:
|
|
32
|
+
js_options = _js_options(options, extra, column_names=column_names)
|
|
33
|
+
return python_from_rpc(await self.js_object.raw(js_options))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class D1Database(Binding):
|
|
37
|
+
def prepare(self, query: str) -> D1PreparedStatement:
|
|
38
|
+
return D1PreparedStatement(self.js_object.prepare(query))
|
|
39
|
+
|
|
40
|
+
async def batch(
|
|
41
|
+
self, statements: Iterable[D1PreparedStatement]
|
|
42
|
+
) -> list[dict[str, Any]]:
|
|
43
|
+
js_statements = python_to_rpc(list(statements))
|
|
44
|
+
return python_from_rpc(await self.js_object.batch(js_statements))
|
|
45
|
+
|
|
46
|
+
async def exec(self, query: str) -> dict[str, Any]:
|
|
47
|
+
return python_from_rpc(await self.js_object.exec(query))
|
workers/bindings/env.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""The env an entrypoint sees, which picks each binding's class."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .._js import js_name
|
|
7
|
+
from ..rpc import python_from_rpc
|
|
8
|
+
from .base import Binding
|
|
9
|
+
from .d1 import D1Database
|
|
10
|
+
from .kv import KVNamespace
|
|
11
|
+
from .queues import Queue
|
|
12
|
+
from .r2 import R2Bucket
|
|
13
|
+
from .services import DurableObjectNamespace, Fetcher
|
|
14
|
+
from .workflow import Workflow
|
|
15
|
+
|
|
16
|
+
__all__: list[str] = []
|
|
17
|
+
|
|
18
|
+
_CLASS_FOR_JS: dict[str, Callable[[Any], Any]] = {
|
|
19
|
+
"Fetcher": Fetcher,
|
|
20
|
+
"DurableObjectNamespace": DurableObjectNamespace,
|
|
21
|
+
"WorkflowImpl": Workflow,
|
|
22
|
+
"KvNamespace": KVNamespace,
|
|
23
|
+
"R2Bucket": R2Bucket,
|
|
24
|
+
"D1Database": D1Database,
|
|
25
|
+
"WorkerQueue": Queue,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _EnvWrapper(Binding):
|
|
30
|
+
"""The env an entrypoint sees: each binding wrapped by its JS class."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def _env(self) -> Any:
|
|
34
|
+
# The stock SDK's private name for the JS env, which its tests read.
|
|
35
|
+
return self.js_object
|
|
36
|
+
|
|
37
|
+
def _pass_through(self, name: str, value: Any) -> Any:
|
|
38
|
+
js_class = js_name(value)
|
|
39
|
+
if js_class is None:
|
|
40
|
+
binding = value # a text var or secret
|
|
41
|
+
elif js_class in ("Object", "Array"):
|
|
42
|
+
# A JSON var, or a plain-object binding such as version metadata.
|
|
43
|
+
binding = python_from_rpc(value)
|
|
44
|
+
else:
|
|
45
|
+
binding = _CLASS_FOR_JS.get(js_class, Binding)(value)
|
|
46
|
+
|
|
47
|
+
# Bindings don't change, so later reads skip __getattr__.
|
|
48
|
+
setattr(self, name, binding)
|
|
49
|
+
|
|
50
|
+
return binding
|
workers/bindings/kv.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Any, Literal, overload
|
|
2
|
+
|
|
3
|
+
from ..rpc import python_from_rpc, python_to_rpc
|
|
4
|
+
from .base import Binding, _js_options
|
|
5
|
+
|
|
6
|
+
__all__ = ["KVNamespace"]
|
|
7
|
+
|
|
8
|
+
type _KVType = Literal["text", "json", "arrayBuffer", "stream"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KVNamespace(Binding):
|
|
12
|
+
@overload
|
|
13
|
+
async def get(
|
|
14
|
+
self,
|
|
15
|
+
key: str,
|
|
16
|
+
type: Literal["text"] = "text",
|
|
17
|
+
/,
|
|
18
|
+
*,
|
|
19
|
+
cache_ttl: int | None = None,
|
|
20
|
+
**options: Any,
|
|
21
|
+
) -> str | None: ...
|
|
22
|
+
@overload
|
|
23
|
+
async def get(
|
|
24
|
+
self,
|
|
25
|
+
key: str,
|
|
26
|
+
type: Literal["json"],
|
|
27
|
+
/,
|
|
28
|
+
*,
|
|
29
|
+
cache_ttl: int | None = None,
|
|
30
|
+
**options: Any,
|
|
31
|
+
) -> Any: ...
|
|
32
|
+
@overload
|
|
33
|
+
async def get(
|
|
34
|
+
self,
|
|
35
|
+
key: str,
|
|
36
|
+
type: Literal["arrayBuffer"],
|
|
37
|
+
/,
|
|
38
|
+
*,
|
|
39
|
+
cache_ttl: int | None = None,
|
|
40
|
+
**options: Any,
|
|
41
|
+
) -> memoryview | None: ...
|
|
42
|
+
@overload
|
|
43
|
+
async def get(
|
|
44
|
+
self,
|
|
45
|
+
key: Any,
|
|
46
|
+
type: str | dict[str, Any] | Any = "text",
|
|
47
|
+
/,
|
|
48
|
+
*,
|
|
49
|
+
cache_ttl: int | None = None,
|
|
50
|
+
**options: Any,
|
|
51
|
+
) -> Any: ...
|
|
52
|
+
async def get(
|
|
53
|
+
self,
|
|
54
|
+
key: Any,
|
|
55
|
+
type: Any = "text",
|
|
56
|
+
/,
|
|
57
|
+
*,
|
|
58
|
+
cache_ttl: int | None = None,
|
|
59
|
+
**options: Any,
|
|
60
|
+
) -> Any:
|
|
61
|
+
js_options = _kv_get_options(type, options, cache_ttl)
|
|
62
|
+
# An arrayBuffer arrives as a memoryview, as in the stock SDK.
|
|
63
|
+
return python_from_rpc(await self.js_object.get(python_to_rpc(key), js_options))
|
|
64
|
+
|
|
65
|
+
async def get_with_metadata(
|
|
66
|
+
self,
|
|
67
|
+
key: Any,
|
|
68
|
+
type: _KVType | dict[str, Any] | Any = "text",
|
|
69
|
+
/,
|
|
70
|
+
*,
|
|
71
|
+
cache_ttl: int | None = None,
|
|
72
|
+
**options: Any,
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
js_options = _kv_get_options(type, options, cache_ttl)
|
|
75
|
+
result = await self.js_object.getWithMetadata(python_to_rpc(key), js_options)
|
|
76
|
+
return python_from_rpc(result)
|
|
77
|
+
|
|
78
|
+
async def put(
|
|
79
|
+
self,
|
|
80
|
+
key: str,
|
|
81
|
+
value: Any,
|
|
82
|
+
options: Any = None,
|
|
83
|
+
/,
|
|
84
|
+
*,
|
|
85
|
+
expiration: int | None = None,
|
|
86
|
+
expiration_ttl: int | None = None,
|
|
87
|
+
metadata: Any = None,
|
|
88
|
+
**extra: Any,
|
|
89
|
+
) -> None:
|
|
90
|
+
js_options = _js_options(
|
|
91
|
+
options,
|
|
92
|
+
extra,
|
|
93
|
+
expiration=expiration,
|
|
94
|
+
expiration_ttl=expiration_ttl,
|
|
95
|
+
metadata=metadata,
|
|
96
|
+
)
|
|
97
|
+
await self.js_object.put(key, python_to_rpc(value), js_options)
|
|
98
|
+
|
|
99
|
+
async def delete(self, key: str) -> None:
|
|
100
|
+
await self.js_object.delete(key)
|
|
101
|
+
|
|
102
|
+
async def list(
|
|
103
|
+
self,
|
|
104
|
+
options: Any = None,
|
|
105
|
+
/,
|
|
106
|
+
*,
|
|
107
|
+
prefix: str | None = None,
|
|
108
|
+
limit: int | None = None,
|
|
109
|
+
cursor: str | None = None,
|
|
110
|
+
**extra: Any,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
js_options = _js_options(
|
|
113
|
+
options, extra, prefix=prefix, limit=limit, cursor=cursor
|
|
114
|
+
)
|
|
115
|
+
return python_from_rpc(await self.js_object.list(js_options))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _kv_get_options(type: Any, extra: dict[str, Any], cache_ttl: int | None) -> Any:
|
|
119
|
+
# The second argument is a type name or, as in JS, an options object.
|
|
120
|
+
given, kv_type = (None, type) if isinstance(type, str) else (type, None)
|
|
121
|
+
return _js_options(given, extra, type=kv_type, cache_ttl=cache_ttl)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..rpc import python_to_rpc
|
|
5
|
+
from .base import Binding, _js_options
|
|
6
|
+
|
|
7
|
+
__all__ = ["Queue"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Queue(Binding):
|
|
11
|
+
async def send(
|
|
12
|
+
self,
|
|
13
|
+
body: Any,
|
|
14
|
+
options: Any = None,
|
|
15
|
+
/,
|
|
16
|
+
*,
|
|
17
|
+
content_type: str | None = None,
|
|
18
|
+
delay_seconds: int | None = None,
|
|
19
|
+
**extra: Any,
|
|
20
|
+
) -> None:
|
|
21
|
+
js_options = _js_options(
|
|
22
|
+
options, extra, content_type=content_type, delay_seconds=delay_seconds
|
|
23
|
+
)
|
|
24
|
+
await self.js_object.send(python_to_rpc(body), js_options)
|
|
25
|
+
|
|
26
|
+
async def send_batch(
|
|
27
|
+
self,
|
|
28
|
+
messages: Iterable[dict[str, Any]],
|
|
29
|
+
options: Any = None,
|
|
30
|
+
/,
|
|
31
|
+
*,
|
|
32
|
+
delay_seconds: int | None = None,
|
|
33
|
+
**extra: Any,
|
|
34
|
+
) -> None:
|
|
35
|
+
js_options = _js_options(options, extra, delay_seconds=delay_seconds)
|
|
36
|
+
await self.js_object.sendBatch(python_to_rpc(list(messages)), js_options)
|
workers/bindings/r2.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..rpc import python_from_rpc, python_to_rpc
|
|
5
|
+
from .base import Binding, _js_options
|
|
6
|
+
|
|
7
|
+
__all__ = ["R2Bucket", "R2Object", "R2ObjectBody"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class R2Object(Binding):
|
|
11
|
+
@property
|
|
12
|
+
def key(self) -> str:
|
|
13
|
+
return self.js_object.key
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def version(self) -> str:
|
|
17
|
+
return self.js_object.version
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def size(self) -> int:
|
|
21
|
+
return self.js_object.size
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def etag(self) -> str:
|
|
25
|
+
return self.js_object.etag
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def http_etag(self) -> str:
|
|
29
|
+
return self.js_object.httpEtag
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def uploaded(self) -> Any:
|
|
33
|
+
return python_from_rpc(self.js_object.uploaded)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def http_metadata(self) -> dict[str, Any]:
|
|
37
|
+
return python_from_rpc(self.js_object.httpMetadata)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def custom_metadata(self) -> dict[str, str]:
|
|
41
|
+
return python_from_rpc(self.js_object.customMetadata)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class R2ObjectBody(R2Object):
|
|
45
|
+
@property
|
|
46
|
+
def body(self) -> Any:
|
|
47
|
+
return self.js_object.body
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def body_used(self) -> bool:
|
|
51
|
+
return self.js_object.bodyUsed
|
|
52
|
+
|
|
53
|
+
async def text(self) -> str:
|
|
54
|
+
return await self.js_object.text()
|
|
55
|
+
|
|
56
|
+
async def bytes(self) -> bytes:
|
|
57
|
+
return (await self.js_object.arrayBuffer()).to_bytes()
|
|
58
|
+
|
|
59
|
+
async def json(self) -> Any:
|
|
60
|
+
return python_from_rpc(await self.js_object.json())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _r2_object(value: Any) -> Any:
|
|
64
|
+
value = python_from_rpc(value)
|
|
65
|
+
if value is None:
|
|
66
|
+
return None
|
|
67
|
+
return R2ObjectBody(value) if hasattr(value, "body") else R2Object(value)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class R2Bucket(Binding):
|
|
71
|
+
async def head(self, key: str) -> R2Object | None:
|
|
72
|
+
return _r2_object(await self.js_object.head(key))
|
|
73
|
+
|
|
74
|
+
async def get(
|
|
75
|
+
self, key: str, options: Any = None, /, **extra: Any
|
|
76
|
+
) -> R2ObjectBody | R2Object | None:
|
|
77
|
+
# An R2Object without a body when an onlyIf condition fails.
|
|
78
|
+
js_options = _js_options(options, extra)
|
|
79
|
+
return _r2_object(await self.js_object.get(key, js_options))
|
|
80
|
+
|
|
81
|
+
async def put(
|
|
82
|
+
self,
|
|
83
|
+
key: str,
|
|
84
|
+
value: Any,
|
|
85
|
+
options: Any = None,
|
|
86
|
+
/,
|
|
87
|
+
*,
|
|
88
|
+
http_metadata: dict[str, Any] | None = None,
|
|
89
|
+
custom_metadata: dict[str, str] | None = None,
|
|
90
|
+
**extra: Any,
|
|
91
|
+
) -> R2Object | None:
|
|
92
|
+
js_options = _js_options(
|
|
93
|
+
options,
|
|
94
|
+
extra,
|
|
95
|
+
http_metadata=http_metadata,
|
|
96
|
+
custom_metadata=custom_metadata,
|
|
97
|
+
)
|
|
98
|
+
return _r2_object(
|
|
99
|
+
await self.js_object.put(key, python_to_rpc(value), js_options)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def delete(self, keys: str | Iterable[str]) -> None:
|
|
103
|
+
await self.js_object.delete(
|
|
104
|
+
keys if isinstance(keys, str) else python_to_rpc(list(keys))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def list(
|
|
108
|
+
self,
|
|
109
|
+
options: Any = None,
|
|
110
|
+
/,
|
|
111
|
+
*,
|
|
112
|
+
prefix: str | None = None,
|
|
113
|
+
limit: int | None = None,
|
|
114
|
+
cursor: str | None = None,
|
|
115
|
+
delimiter: str | None = None,
|
|
116
|
+
**extra: Any,
|
|
117
|
+
) -> dict[str, Any]:
|
|
118
|
+
js_options = _js_options(
|
|
119
|
+
options,
|
|
120
|
+
extra,
|
|
121
|
+
prefix=prefix,
|
|
122
|
+
limit=limit,
|
|
123
|
+
cursor=cursor,
|
|
124
|
+
delimiter=delimiter,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
listing = python_from_rpc(await self.js_object.list(js_options))
|
|
128
|
+
listing["objects"] = [R2Object(item) for item in listing.get("objects", [])]
|
|
129
|
+
|
|
130
|
+
return listing
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Service bindings, the assets binding and Durable Object namespaces."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..fetch import fetch
|
|
6
|
+
from ..response import Response
|
|
7
|
+
from .base import Binding, _js_options
|
|
8
|
+
|
|
9
|
+
__all__ = ["DurableObjectNamespace", "Fetcher"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Fetcher(Binding):
|
|
13
|
+
"""A service binding, the assets binding, or a Durable Object stub.
|
|
14
|
+
|
|
15
|
+
`fetch` returns a crosswind Response. RPC methods pass through.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
async def fetch(self, input: Any, **options: Any) -> Response:
|
|
19
|
+
return await fetch(input, fetcher=self.js_object.fetch, **options)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _stub(js_stub: Any) -> Any:
|
|
23
|
+
# Callers type it as the Durable Object class, which no Python type can
|
|
24
|
+
# say of a stub (decisions.md#typed-bindings).
|
|
25
|
+
return Fetcher(js_stub)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DurableObjectNamespace[T](Binding):
|
|
29
|
+
"""Names this class doesn't define come back raw, as in the stock SDK, so
|
|
30
|
+
a Durable Object id from `idFromName` stays a JS id."""
|
|
31
|
+
|
|
32
|
+
def _pass_through(self, name: str, value: Any) -> Any:
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def id_from_name(self, name: str) -> Any:
|
|
36
|
+
return self.js_object.idFromName(name)
|
|
37
|
+
|
|
38
|
+
def id_from_string(self, id: str) -> Any:
|
|
39
|
+
return self.js_object.idFromString(id)
|
|
40
|
+
|
|
41
|
+
def new_unique_id(self, options: Any = None, /, **extra: Any) -> Any:
|
|
42
|
+
return self.js_object.newUniqueId(_js_options(options, extra))
|
|
43
|
+
|
|
44
|
+
def get(self, id: Any, options: Any = None, /, **extra: Any) -> T:
|
|
45
|
+
return _stub(self.js_object.get(id, _js_options(options, extra)))
|
|
46
|
+
|
|
47
|
+
def get_by_name(self, name: str, options: Any = None, /, **extra: Any) -> T:
|
|
48
|
+
return _stub(self.js_object.getByName(name, _js_options(options, extra)))
|
|
49
|
+
|
|
50
|
+
def getByName(self, name: str, *args: Any, **kwargs: Any) -> T: # noqa: N802
|
|
51
|
+
# The stock SDK's name, which also returns a stub with a Python fetch.
|
|
52
|
+
return _stub(self.js_object.getByName(name, *args, **kwargs))
|
|
53
|
+
|
|
54
|
+
def jurisdiction(self, name: str) -> "DurableObjectNamespace[T]":
|
|
55
|
+
return DurableObjectNamespace(self.js_object.jurisdiction(name))
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""The Workflow binding, which starts and controls instances. The Workflow
|
|
2
|
+
code itself lives in `workers.workflows`."""
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..rpc import python_from_rpc, python_to_rpc
|
|
8
|
+
from .base import Binding, _js_options
|
|
9
|
+
|
|
10
|
+
__all__ = ["Workflow", "WorkflowInstance"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkflowInstance(Binding):
|
|
14
|
+
@property
|
|
15
|
+
def id(self) -> str:
|
|
16
|
+
return self.js_object.id
|
|
17
|
+
|
|
18
|
+
async def status(self) -> dict[str, Any]:
|
|
19
|
+
return python_from_rpc(await self.js_object.status())
|
|
20
|
+
|
|
21
|
+
async def pause(self) -> None:
|
|
22
|
+
await self.js_object.pause()
|
|
23
|
+
|
|
24
|
+
async def resume(self) -> None:
|
|
25
|
+
await self.js_object.resume()
|
|
26
|
+
|
|
27
|
+
async def restart(self) -> None:
|
|
28
|
+
await self.js_object.restart()
|
|
29
|
+
|
|
30
|
+
async def terminate(self) -> None:
|
|
31
|
+
await self.js_object.terminate()
|
|
32
|
+
|
|
33
|
+
async def send_event(
|
|
34
|
+
self,
|
|
35
|
+
event: Any = None,
|
|
36
|
+
/,
|
|
37
|
+
*,
|
|
38
|
+
type: str | None = None,
|
|
39
|
+
payload: Any = None,
|
|
40
|
+
**extra: Any,
|
|
41
|
+
) -> None:
|
|
42
|
+
js_event = _js_options(event, extra, type=type, payload=payload)
|
|
43
|
+
await self.js_object.sendEvent(js_event)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Workflow(Binding):
|
|
47
|
+
async def get(self, id: str) -> WorkflowInstance:
|
|
48
|
+
return WorkflowInstance(await self.js_object.get(id))
|
|
49
|
+
|
|
50
|
+
async def create(
|
|
51
|
+
self,
|
|
52
|
+
options: Any = None,
|
|
53
|
+
/,
|
|
54
|
+
*,
|
|
55
|
+
id: str | None = None,
|
|
56
|
+
params: Any = None,
|
|
57
|
+
**extra: Any,
|
|
58
|
+
) -> WorkflowInstance:
|
|
59
|
+
js_options = _js_options(options, extra, id=id, params=params)
|
|
60
|
+
return WorkflowInstance(await self.js_object.create(js_options))
|
|
61
|
+
|
|
62
|
+
async def create_batch(self, batch: Iterable[Any]) -> list[WorkflowInstance]:
|
|
63
|
+
instances = await self.js_object.createBatch(python_to_rpc(list(batch)))
|
|
64
|
+
return [WorkflowInstance(instance) for instance in instances]
|
workers/blob.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from collections.abc import Generator, Iterable
|
|
2
|
+
from contextlib import ExitStack, contextmanager
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import js
|
|
7
|
+
from pyodide.ffi import to_js
|
|
8
|
+
|
|
9
|
+
from ._js import js_name, lend_buffer, managed_pyproxies
|
|
10
|
+
|
|
11
|
+
__all__ = ["Blob", "BlobEnding", "BlobValue", "File"]
|
|
12
|
+
|
|
13
|
+
type BlobValue = (
|
|
14
|
+
str | bytes | js.ArrayBuffer | js.TypedArray | js.DataView | js.Blob | Blob | File
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BlobEnding(StrEnum):
|
|
19
|
+
TRANSPARENT = "transparent"
|
|
20
|
+
NATIVE = "native"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Blob:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
blob_parts: Iterable[BlobValue] | BlobValue,
|
|
27
|
+
content_type: str | None = None,
|
|
28
|
+
endings: BlobEnding | str | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
if not _is_part_list(blob_parts):
|
|
31
|
+
# A single Blob or File passes on its type. A File's name is lost.
|
|
32
|
+
if not content_type:
|
|
33
|
+
content_type = _blob_type(blob_parts)
|
|
34
|
+
blob_parts = [blob_parts]
|
|
35
|
+
|
|
36
|
+
with _js_parts(blob_parts) as parts:
|
|
37
|
+
self._js_blob = js.Blob.new(
|
|
38
|
+
parts, type=content_type, endings=str(endings) if endings else None
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def size(self) -> int:
|
|
43
|
+
return self._js_blob.size
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def content_type(self) -> str:
|
|
47
|
+
return self._js_blob.type
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def js_object(self) -> "js.Blob":
|
|
51
|
+
return self._js_blob
|
|
52
|
+
|
|
53
|
+
async def text(self) -> str:
|
|
54
|
+
return await self.js_object.text()
|
|
55
|
+
|
|
56
|
+
async def bytes(self) -> bytes:
|
|
57
|
+
return (await self.js_object.arrayBuffer()).to_bytes()
|
|
58
|
+
|
|
59
|
+
def slice(
|
|
60
|
+
self,
|
|
61
|
+
start: int | None = None,
|
|
62
|
+
end: int | None = None,
|
|
63
|
+
content_type: str | None = None,
|
|
64
|
+
) -> "Blob":
|
|
65
|
+
return Blob([self.js_object.slice(start, end, content_type)])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class File(Blob):
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
blob_parts: Iterable[BlobValue] | BlobValue,
|
|
72
|
+
filename: str | None = None,
|
|
73
|
+
content_type: str | None = None,
|
|
74
|
+
endings: BlobEnding | str | None = None,
|
|
75
|
+
last_modified: int | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
if not _is_part_list(blob_parts):
|
|
78
|
+
# A single Blob or File passes on its type, and a File its name and
|
|
79
|
+
# time, so a JS File received over RPC wraps without a filename.
|
|
80
|
+
if not content_type:
|
|
81
|
+
content_type = _blob_type(blob_parts)
|
|
82
|
+
source: Any = blob_parts
|
|
83
|
+
if isinstance(source, File):
|
|
84
|
+
source = source.js_object
|
|
85
|
+
if js_name(source) == "File":
|
|
86
|
+
filename = source.name if filename is None else filename
|
|
87
|
+
last_modified = last_modified or source.lastModified
|
|
88
|
+
blob_parts = [blob_parts]
|
|
89
|
+
|
|
90
|
+
if filename is None:
|
|
91
|
+
raise TypeError("File() needs a filename unless it wraps a single File")
|
|
92
|
+
|
|
93
|
+
with _js_parts(blob_parts) as parts:
|
|
94
|
+
self._js_blob = js.File.new(
|
|
95
|
+
parts,
|
|
96
|
+
filename,
|
|
97
|
+
type=content_type,
|
|
98
|
+
endings=str(endings) if endings else None,
|
|
99
|
+
lastModified=last_modified,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
return self._js_blob.name
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def last_modified(self) -> int:
|
|
108
|
+
return self._js_blob.lastModified
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _is_part_list(parts: Any) -> bool:
|
|
112
|
+
# bytes-like values are one part, even though Python can iterate them.
|
|
113
|
+
if isinstance(parts, (str, bytes, bytearray, memoryview)):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
iter(parts)
|
|
118
|
+
except TypeError:
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _blob_type(part: Any) -> str | None:
|
|
125
|
+
if isinstance(part, Blob):
|
|
126
|
+
return part.content_type
|
|
127
|
+
if js_name(part) in ("Blob", "File"):
|
|
128
|
+
return part.type
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@contextmanager
|
|
133
|
+
def _js_parts(parts: Any) -> Generator[Any]:
|
|
134
|
+
with ExitStack() as stack:
|
|
135
|
+
entries = [stack.enter_context(_js_part(part)) for part in parts]
|
|
136
|
+
with managed_pyproxies() as pyproxies:
|
|
137
|
+
yield to_js(entries, pyproxies=pyproxies)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@contextmanager
|
|
141
|
+
def _js_part(part: Any) -> Generator[Any]:
|
|
142
|
+
if isinstance(part, str) or js_name(part) in ("Blob", "File"):
|
|
143
|
+
yield part
|
|
144
|
+
elif isinstance(part, Blob):
|
|
145
|
+
yield part.js_object
|
|
146
|
+
elif _is_buffer(part):
|
|
147
|
+
with lend_buffer(part) as view:
|
|
148
|
+
yield view
|
|
149
|
+
else:
|
|
150
|
+
raise TypeError(f"Don't know how to handle {type(part)} for Blob()")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_buffer(value: Any) -> bool:
|
|
154
|
+
try:
|
|
155
|
+
with memoryview(value):
|
|
156
|
+
return True
|
|
157
|
+
except TypeError:
|
|
158
|
+
return False
|