workers-runtime-sdk 1.2.0__tar.gz → 1.3.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.3.0}/CHANGELOG.md +17 -0
  2. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/PKG-INFO +1 -1
  3. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/pyproject.toml +1 -1
  4. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/workers/_workers.py +114 -13
  5. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/.gitignore +0 -0
  6. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/AGENTS.md +0 -0
  7. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/README.md +0 -0
  8. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/_cloudflare_compat_flags.pyi +0 -0
  9. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/_pyodide_entrypoint_helper.pyi +0 -0
  10. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/_workers_sdk_entropy_import_context.pth +0 -0
  11. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/_workers_sdk_entropy_import_context.py +0 -0
  12. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/_workers_sdk_entropy_import_context_loader.py +0 -0
  13. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/asgi.py +0 -0
  14. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/workers/__init__.py +0 -0
  15. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/workers/py.typed +0 -0
  16. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/src/workers/workflows.py +0 -0
  17. {workers_runtime_sdk-1.2.0 → workers_runtime_sdk-1.3.0}/uv.lock +0 -0
@@ -2,6 +2,23 @@
2
2
 
3
3
  <!-- version list -->
4
4
 
5
+ ## v1.3.0 (2026-06-15)
6
+
7
+ ### Features
8
+
9
+ - **runtime-sdk**: Revise type conversion for Durable Object binding
10
+ ([#112](https://github.com/cloudflare/workers-py/pull/112),
11
+ [`b12650e`](https://github.com/cloudflare/workers-py/commit/b12650ef91bb71f4ebebd9827bad2d1f0946fd62))
12
+
13
+ - **runtime-sdk**: Revise type conversion to support bindings more natively
14
+ ([#112](https://github.com/cloudflare/workers-py/pull/112),
15
+ [`b12650e`](https://github.com/cloudflare/workers-py/commit/b12650ef91bb71f4ebebd9827bad2d1f0946fd62))
16
+
17
+ - **runtime-sdk**: Update js object conversion logic to support cloudflare bindings more natively.
18
+ ([#112](https://github.com/cloudflare/workers-py/pull/112),
19
+ [`b12650e`](https://github.com/cloudflare/workers-py/commit/b12650ef91bb71f4ebebd9827bad2d1f0946fd62))
20
+
21
+
5
22
  ## v1.2.0 (2026-06-12)
6
23
 
7
24
  ### 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.3.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.3.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
@@ -916,6 +915,9 @@ class Request:
916
915
 
917
916
 
918
917
  def _python_from_rpc_default_converter(value, convert, cache):
918
+ if value is jsnull:
919
+ return None
920
+
919
921
  if not hasattr(value, "constructor"):
920
922
  # Assume that the object doesn't need conversion as it's not a JS object.
921
923
  return value
@@ -947,6 +949,41 @@ def _python_from_rpc_default_converter(value, convert, cache):
947
949
  return value
948
950
 
949
951
 
952
+ class JsDict(dict):
953
+ """
954
+ Python dictionary that allows attribute access to keys.
955
+
956
+ This is used to convert JS objects to Python dictionaries while maintaining
957
+ the ability to access keys as attributes.
958
+ """
959
+
960
+ def __getattr__(self, name):
961
+ # The limitation of this approach is that if there is a key that conflicts with a built-in
962
+ # method or attribute of the dict class, it will not be accessible through attribute access.
963
+ # But that is a reasonable trade-off for the convenience of being able to access keys as
964
+ # attributes.
965
+ try:
966
+ return self[name]
967
+ except KeyError:
968
+ raise AttributeError(name) from None
969
+
970
+ def __setattr__(self, name, value):
971
+ self[name] = value
972
+
973
+
974
+ def _replace_jsnull_with_none(obj):
975
+ """
976
+ Recursively converts JS objects to Python objects.
977
+ """
978
+ if obj is jsnull:
979
+ return None
980
+ if isinstance(obj, dict):
981
+ return JsDict({k: _replace_jsnull_with_none(v) for k, v in obj.items()})
982
+ if isinstance(obj, list):
983
+ return [_replace_jsnull_with_none(v) for v in obj]
984
+ return obj
985
+
986
+
950
987
  def python_from_rpc(obj: "JsProxy"):
951
988
  """
952
989
  Converts JS objects like Response, Request, Blob, etc. to equivalent Python objects defined in
@@ -956,6 +993,9 @@ def python_from_rpc(obj: "JsProxy"):
956
993
  it does not support serializing all JS object types.
957
994
  """
958
995
 
996
+ if obj is jsnull:
997
+ return None
998
+
959
999
  if not hasattr(obj, "constructor"):
960
1000
  return obj
961
1001
 
@@ -966,14 +1006,20 @@ def python_from_rpc(obj: "JsProxy"):
966
1006
 
967
1007
  result = obj.to_py(default_converter=_python_from_rpc_default_converter)
968
1008
 
969
- return result
1009
+ return _replace_jsnull_with_none(result)
970
1010
 
971
1011
 
972
1012
  def _raise_on_disabled_type(value):
1013
+ if isinstance(value, _BindingWrapper):
1014
+ return
1015
+
1016
+ if callable(value) and not isinstance(value, type):
1017
+ return
1018
+
973
1019
  if _is_js_instance(value, "RegExp"):
974
1020
  raise TypeError(f"{value.constructor.name} cannot be sent over RPC.")
975
1021
 
976
- if isinstance(value, (tuple, bytearray, LambdaType)):
1022
+ if isinstance(value, (tuple, bytearray)):
977
1023
  raise TypeError(f"{type(value)} cannot be sent over RPC.")
978
1024
 
979
1025
  if inspect.isawaitable(value):
@@ -991,7 +1037,10 @@ def _raise_on_disabled_type(value):
991
1037
 
992
1038
  def _python_to_rpc_default_converter(obj, convert, cache):
993
1039
  if obj is None:
994
- return obj
1040
+ return jsnull
1041
+
1042
+ if isinstance(obj, _BindingWrapper):
1043
+ return obj._binding
995
1044
 
996
1045
  if hasattr(obj, "js_object"):
997
1046
  return obj.js_object
@@ -1003,11 +1052,26 @@ def _python_to_rpc_default_converter(obj, convert, cache):
1003
1052
  if isinstance(obj, Exception):
1004
1053
  return js.Error.new(str(obj))
1005
1054
 
1055
+ if callable(obj) and not isinstance(obj, type):
1056
+ # Wrap function with create_proxy so that
1057
+ # it doesn't get garbage collected
1058
+ return create_proxy(obj)
1059
+
1006
1060
  _raise_on_disabled_type(obj)
1007
1061
 
1008
1062
  return obj
1009
1063
 
1010
1064
 
1065
+ def _replace_none_with_jsnull(value):
1066
+ if value is None:
1067
+ return jsnull
1068
+ if isinstance(value, dict):
1069
+ return {k: _replace_none_with_jsnull(v) for k, v in value.items()}
1070
+ if isinstance(value, list):
1071
+ return [_replace_none_with_jsnull(v) for v in value]
1072
+ return value
1073
+
1074
+
1011
1075
  def python_to_rpc(value) -> JsProxy:
1012
1076
  """
1013
1077
  Converts Python objects defined in this module (Response, Request, etc) and native Python types
@@ -1017,37 +1081,58 @@ def python_to_rpc(value) -> JsProxy:
1017
1081
  it does not support serializing all Python object types.
1018
1082
  """
1019
1083
 
1084
+ if value is None:
1085
+ return jsnull
1086
+
1087
+ if isinstance(value, _BindingWrapper):
1088
+ return value._binding
1089
+
1090
+ value = _replace_none_with_jsnull(value)
1091
+
1020
1092
  # `to_js` won't always call the default_converter, for example when a list of tuples is passed
1021
1093
  _raise_on_disabled_type(value)
1022
1094
 
1023
1095
  result = to_js(
1024
1096
  value,
1025
1097
  default_converter=_python_to_rpc_default_converter,
1026
- dict_converter=js.Map.new,
1098
+ dict_converter=Object.fromEntries,
1027
1099
  )
1028
1100
 
1029
1101
  return result
1030
1102
 
1031
1103
 
1032
- class _FetcherWrapper:
1104
+ class _BindingWrapper:
1033
1105
  def __init__(self, binding):
1034
1106
  self._binding = binding
1035
1107
 
1108
+ def _convert_result(self, result):
1109
+ converted = python_from_rpc(result)
1110
+ if isinstance(converted, JsProxy):
1111
+ # If the RPC result is another JsProxy, we assume that
1112
+ # it is another RPC-wrapped object and wrap it as well.
1113
+ # for example, d1.bind() returns the same object as a result.
1114
+ # TODO: This is a bit of a hack. We should revisit when there are more
1115
+ # bindings to support with different patterns.
1116
+ return self.__class__(converted)
1117
+ return converted
1118
+
1036
1119
  def _getattr_helper(self, name):
1037
1120
  attr = getattr(self._binding, name)
1038
1121
 
1039
1122
  if not callable(attr):
1040
- return attr
1123
+ return self._convert_result(attr)
1041
1124
 
1042
- # Not using `@functools.wraps(attr)` here because `attr` is a JS proxy.
1043
- async def wrapper(*args, **kwargs):
1125
+ def wrapper(*args, **kwargs):
1044
1126
  js_args = [python_to_rpc(arg) for arg in args]
1045
1127
  js_kwargs = {k: python_to_rpc(v) for k, v in kwargs.items()}
1046
1128
  result = attr(*js_args, **js_kwargs)
1047
1129
  if hasattr(result, "then") and callable(result.then):
1048
- return python_from_rpc(await result)
1049
- else:
1050
- return python_from_rpc(result)
1130
+
1131
+ async def await_and_convert():
1132
+ return self._convert_result(await result)
1133
+
1134
+ return await_and_convert()
1135
+ return self._convert_result(result)
1051
1136
 
1052
1137
  return wrapper
1053
1138
 
@@ -1056,6 +1141,11 @@ class _FetcherWrapper:
1056
1141
  setattr(self, name, result)
1057
1142
  return result
1058
1143
 
1144
+ def __getitem__(self, key):
1145
+ return self._convert_result(getattr(self._binding, key))
1146
+
1147
+
1148
+ class _FetcherWrapper(_BindingWrapper):
1059
1149
  def fetch(self, *args, **kwargs):
1060
1150
  return fetch(*args, fetcher=self._binding.fetch, **kwargs)
1061
1151
 
@@ -1089,6 +1179,9 @@ class DurableObjectContext:
1089
1179
 
1090
1180
  def __getattr__(self, name: str):
1091
1181
  result = getattr(self._ctx, name)
1182
+ if _is_js_instance(result, "DurableObjectStorage"):
1183
+ # durable_object.ctx.storage
1184
+ result = _BindingWrapper(result)
1092
1185
  setattr(self, name, result)
1093
1186
  return result
1094
1187
 
@@ -1173,7 +1266,15 @@ class _EnvWrapper:
1173
1266
  if _is_js_instance(binding, "WorkflowImpl"):
1174
1267
  return _WorkflowBindingWrapper(binding)
1175
1268
 
1176
- # TODO: Implement APIs for bindings.
1269
+ if _is_js_instance(binding, "KvNamespace"):
1270
+ return _BindingWrapper(binding)
1271
+
1272
+ if _is_js_instance(binding, "R2Bucket"):
1273
+ return _BindingWrapper(binding)
1274
+
1275
+ if _is_js_instance(binding, "D1Database"):
1276
+ return _BindingWrapper(binding)
1277
+
1177
1278
  return binding
1178
1279
 
1179
1280
  def __getattr__(self, name):