workers-runtime-sdk 1.2.0__tar.gz → 1.4.0__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.
Files changed (17) hide show
  1. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/CHANGELOG.md +30 -0
  2. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/PKG-INFO +1 -1
  3. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/pyproject.toml +1 -1
  4. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/workers/_workers.py +146 -15
  5. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/.gitignore +0 -0
  6. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/AGENTS.md +0 -0
  7. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/README.md +0 -0
  8. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/_cloudflare_compat_flags.pyi +0 -0
  9. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/_pyodide_entrypoint_helper.pyi +0 -0
  10. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/_workers_sdk_entropy_import_context.pth +0 -0
  11. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/_workers_sdk_entropy_import_context.py +0 -0
  12. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
  13. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/asgi.py +0 -0
  14. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/workers/__init__.py +0 -0
  15. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/workers/py.typed +0 -0
  16. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/src/workers/workflows.py +0 -0
  17. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.4.0}/uv.lock +0 -0
@@ -2,6 +2,36 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.4.0 (2026-06-17)
6
+
7
+ ### Features
8
+
9
+ - Auto-convert Python objects that are passed to/from Queue
10
+ ([#123](https://github.com/cloudflare/workers-py/pull/123),
11
+ [`906a10a`](https://github.com/cloudflare/workers-py/commit/906a10a7392f9d823a1b6bba044300ece8763401))
12
+
13
+ - Auto-convert Python objects that are passed to/from Queue Binding
14
+ ([#123](https://github.com/cloudflare/workers-py/pull/123),
15
+ [`906a10a`](https://github.com/cloudflare/workers-py/commit/906a10a7392f9d823a1b6bba044300ece8763401))
16
+
17
+
18
+ ## v1.3.0 (2026-06-15)
19
+
20
+ ### Features
21
+
22
+ - **runtime-sdk**: Revise type conversion for Durable Object binding
23
+ ([#112](https://github.com/cloudflare/workers-py/pull/112),
24
+ [`b12650e`](https://github.com/cloudflare/workers-py/commit/b12650ef91bb71f4ebebd9827bad2d1f0946fd62))
25
+
26
+ - **runtime-sdk**: Revise type conversion to support bindings more natively
27
+ ([#112](https://github.com/cloudflare/workers-py/pull/112),
28
+ [`b12650e`](https://github.com/cloudflare/workers-py/commit/b12650ef91bb71f4ebebd9827bad2d1f0946fd62))
29
+
30
+ - **runtime-sdk**: Update js object conversion logic to support cloudflare bindings more natively.
31
+ ([#112](https://github.com/cloudflare/workers-py/pull/112),
32
+ [`b12650e`](https://github.com/cloudflare/workers-py/commit/b12650ef91bb71f4ebebd9827bad2d1f0946fd62))
33
+
34
+
5
35
  ## v1.2.0 (2026-06-12)
6
36
 
7
37
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workers-runtime-sdk
3
- Version: 1.2.0
3
+ Version: 1.4.0
4
4
  Summary: Python SDK for Cloudflare Workers
5
5
  Project-URL: Homepage, https://github.com/cloudflare/workers-py
6
6
  Project-URL: Bug Tracker, https://github.com/cloudflare/workers-py/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "workers-runtime-sdk"
7
- version = "1.2.0"
7
+ version = "1.4.0"
8
8
  description = "Python SDK for Cloudflare Workers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -17,7 +17,6 @@ from collections.abc import (
17
17
  from contextlib import ExitStack, contextmanager
18
18
  from enum import StrEnum
19
19
  from http import HTTPMethod, HTTPStatus
20
- from types import LambdaType
21
20
  from typing import TYPE_CHECKING, Any, Never, Protocol, TypedDict, Unpack
22
21
 
23
22
  import _cloudflare_compat_flags
@@ -320,8 +319,13 @@ def _manage_pyproxies():
320
319
  destroy_proxies(proxies)
321
320
 
322
321
 
323
- def _is_js_instance(val, js_cls_name):
324
- return hasattr(val, "constructor") and val.constructor.name == js_cls_name
322
+ def _is_js_instance(val, js_cls_names: str | set[str]):
323
+ if not hasattr(val, "constructor"):
324
+ return False
325
+ name = val.constructor.name
326
+ if isinstance(js_cls_names, set):
327
+ return name in js_cls_names
328
+ return name == js_cls_names
325
329
 
326
330
 
327
331
  try:
@@ -916,6 +920,9 @@ class Request:
916
920
 
917
921
 
918
922
  def _python_from_rpc_default_converter(value, convert, cache):
923
+ if value is jsnull:
924
+ return None
925
+
919
926
  if not hasattr(value, "constructor"):
920
927
  # Assume that the object doesn't need conversion as it's not a JS object.
921
928
  return value
@@ -947,6 +954,41 @@ def _python_from_rpc_default_converter(value, convert, cache):
947
954
  return value
948
955
 
949
956
 
957
+ class JsDict(dict):
958
+ """
959
+ Python dictionary that allows attribute access to keys.
960
+
961
+ This is used to convert JS objects to Python dictionaries while maintaining
962
+ the ability to access keys as attributes.
963
+ """
964
+
965
+ def __getattr__(self, name):
966
+ # The limitation of this approach is that if there is a key that conflicts with a built-in
967
+ # method or attribute of the dict class, it will not be accessible through attribute access.
968
+ # But that is a reasonable trade-off for the convenience of being able to access keys as
969
+ # attributes.
970
+ try:
971
+ return self[name]
972
+ except KeyError:
973
+ raise AttributeError(name) from None
974
+
975
+ def __setattr__(self, name, value):
976
+ self[name] = value
977
+
978
+
979
+ def _replace_jsnull_with_none(obj):
980
+ """
981
+ Recursively converts JS objects to Python objects.
982
+ """
983
+ if obj is jsnull:
984
+ return None
985
+ if isinstance(obj, dict):
986
+ return JsDict({k: _replace_jsnull_with_none(v) for k, v in obj.items()})
987
+ if isinstance(obj, list):
988
+ return [_replace_jsnull_with_none(v) for v in obj]
989
+ return obj
990
+
991
+
950
992
  def python_from_rpc(obj: "JsProxy"):
951
993
  """
952
994
  Converts JS objects like Response, Request, Blob, etc. to equivalent Python objects defined in
@@ -956,6 +998,9 @@ def python_from_rpc(obj: "JsProxy"):
956
998
  it does not support serializing all JS object types.
957
999
  """
958
1000
 
1001
+ if obj is jsnull:
1002
+ return None
1003
+
959
1004
  if not hasattr(obj, "constructor"):
960
1005
  return obj
961
1006
 
@@ -966,14 +1011,20 @@ def python_from_rpc(obj: "JsProxy"):
966
1011
 
967
1012
  result = obj.to_py(default_converter=_python_from_rpc_default_converter)
968
1013
 
969
- return result
1014
+ return _replace_jsnull_with_none(result)
970
1015
 
971
1016
 
972
1017
  def _raise_on_disabled_type(value):
1018
+ if isinstance(value, _BindingWrapper):
1019
+ return
1020
+
1021
+ if callable(value) and not isinstance(value, type):
1022
+ return
1023
+
973
1024
  if _is_js_instance(value, "RegExp"):
974
1025
  raise TypeError(f"{value.constructor.name} cannot be sent over RPC.")
975
1026
 
976
- if isinstance(value, (tuple, bytearray, LambdaType)):
1027
+ if isinstance(value, (tuple, bytearray)):
977
1028
  raise TypeError(f"{type(value)} cannot be sent over RPC.")
978
1029
 
979
1030
  if inspect.isawaitable(value):
@@ -991,7 +1042,10 @@ def _raise_on_disabled_type(value):
991
1042
 
992
1043
  def _python_to_rpc_default_converter(obj, convert, cache):
993
1044
  if obj is None:
994
- return obj
1045
+ return jsnull
1046
+
1047
+ if isinstance(obj, _BindingWrapper):
1048
+ return obj._binding
995
1049
 
996
1050
  if hasattr(obj, "js_object"):
997
1051
  return obj.js_object
@@ -1003,11 +1057,26 @@ def _python_to_rpc_default_converter(obj, convert, cache):
1003
1057
  if isinstance(obj, Exception):
1004
1058
  return js.Error.new(str(obj))
1005
1059
 
1060
+ if callable(obj) and not isinstance(obj, type):
1061
+ # Wrap function with create_proxy so that
1062
+ # it doesn't get garbage collected
1063
+ return create_proxy(obj)
1064
+
1006
1065
  _raise_on_disabled_type(obj)
1007
1066
 
1008
1067
  return obj
1009
1068
 
1010
1069
 
1070
+ def _replace_none_with_jsnull(value):
1071
+ if value is None:
1072
+ return jsnull
1073
+ if isinstance(value, dict):
1074
+ return {k: _replace_none_with_jsnull(v) for k, v in value.items()}
1075
+ if isinstance(value, list):
1076
+ return [_replace_none_with_jsnull(v) for v in value]
1077
+ return value
1078
+
1079
+
1011
1080
  def python_to_rpc(value) -> JsProxy:
1012
1081
  """
1013
1082
  Converts Python objects defined in this module (Response, Request, etc) and native Python types
@@ -1017,37 +1086,65 @@ def python_to_rpc(value) -> JsProxy:
1017
1086
  it does not support serializing all Python object types.
1018
1087
  """
1019
1088
 
1089
+ if value is None:
1090
+ return jsnull
1091
+
1092
+ if isinstance(value, _BindingWrapper):
1093
+ return value._binding
1094
+
1095
+ value = _replace_none_with_jsnull(value)
1096
+
1020
1097
  # `to_js` won't always call the default_converter, for example when a list of tuples is passed
1021
1098
  _raise_on_disabled_type(value)
1022
1099
 
1023
1100
  result = to_js(
1024
1101
  value,
1025
1102
  default_converter=_python_to_rpc_default_converter,
1026
- dict_converter=js.Map.new,
1103
+ dict_converter=Object.fromEntries,
1027
1104
  )
1028
1105
 
1029
1106
  return result
1030
1107
 
1031
1108
 
1032
- class _FetcherWrapper:
1109
+ class _BindingWrapper:
1033
1110
  def __init__(self, binding):
1034
1111
  self._binding = binding
1035
1112
 
1113
+ def _convert_result(self, result):
1114
+ converted = python_from_rpc(result)
1115
+
1116
+ # After python_from_rpc, some objects may still be JsProxy objects.
1117
+ # For now, we wrap all of them with the _BindingWrapper (or a subclass of it)
1118
+ # so that accessing attributes on them will be properly converted.
1119
+
1120
+ # TODO: This is a bit of a hack. We should revisit when there are more
1121
+ # bindings to support with different return types.
1122
+ if isinstance(converted, JsProxy):
1123
+ return self.__class__(converted)
1124
+ if isinstance(converted, list):
1125
+ return [
1126
+ self.__class__(item) if isinstance(item, JsProxy) else item
1127
+ for item in converted
1128
+ ]
1129
+ return converted
1130
+
1036
1131
  def _getattr_helper(self, name):
1037
1132
  attr = getattr(self._binding, name)
1038
1133
 
1039
1134
  if not callable(attr):
1040
- return attr
1135
+ return self._convert_result(attr)
1041
1136
 
1042
- # Not using `@functools.wraps(attr)` here because `attr` is a JS proxy.
1043
- async def wrapper(*args, **kwargs):
1137
+ def wrapper(*args, **kwargs):
1044
1138
  js_args = [python_to_rpc(arg) for arg in args]
1045
1139
  js_kwargs = {k: python_to_rpc(v) for k, v in kwargs.items()}
1046
1140
  result = attr(*js_args, **js_kwargs)
1047
1141
  if hasattr(result, "then") and callable(result.then):
1048
- return python_from_rpc(await result)
1049
- else:
1050
- return python_from_rpc(result)
1142
+
1143
+ async def await_and_convert():
1144
+ return self._convert_result(await result)
1145
+
1146
+ return await_and_convert()
1147
+ return self._convert_result(result)
1051
1148
 
1052
1149
  return wrapper
1053
1150
 
@@ -1056,6 +1153,11 @@ class _FetcherWrapper:
1056
1153
  setattr(self, name, result)
1057
1154
  return result
1058
1155
 
1156
+ def __getitem__(self, key):
1157
+ return self._convert_result(getattr(self._binding, key))
1158
+
1159
+
1160
+ class _FetcherWrapper(_BindingWrapper):
1059
1161
  def fetch(self, *args, **kwargs):
1060
1162
  return fetch(*args, fetcher=self._binding.fetch, **kwargs)
1061
1163
 
@@ -1089,6 +1191,9 @@ class DurableObjectContext:
1089
1191
 
1090
1192
  def __getattr__(self, name: str):
1091
1193
  result = getattr(self._ctx, name)
1194
+ if _is_js_instance(result, "DurableObjectStorage"):
1195
+ # durable_object.ctx.storage
1196
+ result = _BindingWrapper(result)
1092
1197
  setattr(self, name, result)
1093
1198
  return result
1094
1199
 
@@ -1159,6 +1264,13 @@ class _WorkflowBindingWrapper:
1159
1264
 
1160
1265
 
1161
1266
  class _EnvWrapper:
1267
+ _BINDING_TYPES = {
1268
+ "KvNamespace",
1269
+ "R2Bucket",
1270
+ "D1Database",
1271
+ "WorkerQueue",
1272
+ }
1273
+
1162
1274
  def __init__(self, env: Any):
1163
1275
  self._env = env
1164
1276
 
@@ -1173,7 +1285,9 @@ class _EnvWrapper:
1173
1285
  if _is_js_instance(binding, "WorkflowImpl"):
1174
1286
  return _WorkflowBindingWrapper(binding)
1175
1287
 
1176
- # TODO: Implement APIs for bindings.
1288
+ if _is_js_instance(binding, self._BINDING_TYPES):
1289
+ return _BindingWrapper(binding)
1290
+
1177
1291
  return binding
1178
1292
 
1179
1293
  def __getattr__(self, name):
@@ -1449,6 +1563,7 @@ class WorkerEntrypoint:
1449
1563
 
1450
1564
  def __init_subclass__(cls, **_kwargs: Any):
1451
1565
  _wrap_subclass(cls)
1566
+ _wrap_queue_handler(cls)
1452
1567
 
1453
1568
 
1454
1569
  class WorkflowEntrypoint:
@@ -1466,3 +1581,19 @@ class WorkflowEntrypoint:
1466
1581
  def __init_subclass__(cls, **_kwargs: Any):
1467
1582
  _wrap_subclass(cls)
1468
1583
  _wrap_workflow_step(cls)
1584
+
1585
+
1586
+ def _wrap_queue_handler(cls):
1587
+ queue_fn = getattr(cls, "queue", None)
1588
+ if queue_fn is None:
1589
+ return
1590
+
1591
+ @functools.wraps(queue_fn)
1592
+ async def wrapped_queue(self, batch, *args, **kwargs):
1593
+ wrapped_batch = _BindingWrapper(batch)
1594
+ result = queue_fn(self, wrapped_batch, *args, **kwargs)
1595
+ if inspect.iscoroutine(result):
1596
+ result = await result
1597
+ return result
1598
+
1599
+ cls.queue = wrapped_queue