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.
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))
@@ -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