graphddb-runtime 1.2.5__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 (22) hide show
  1. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/PKG-INFO +1 -1
  2. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/leaf_handlers.py +34 -11
  3. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/runtime.py +2 -3
  4. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime.egg-info/PKG-INFO +1 -1
  5. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/pyproject.toml +1 -1
  6. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/tests/test_leaf_handlers.py +64 -0
  7. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/README.md +0 -0
  8. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/__init__.py +0 -0
  9. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/batch.py +0 -0
  10. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/cursor.py +0 -0
  11. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/errors.py +0 -0
  12. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/limits.py +0 -0
  13. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/per_key_cursor.py +0 -0
  14. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime/transport.py +0 -0
  15. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
  16. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
  17. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime.egg-info/requires.txt +0 -0
  18. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/graphddb_runtime.egg-info/top_level.txt +0 -0
  19. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/setup.cfg +0 -0
  20. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/tests/test_crosslang_graphddb_cell.py +0 -0
  21. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/tests/test_crosslang_python_cells.py +0 -0
  22. {graphddb_runtime-1.2.5 → graphddb_runtime-1.4.0}/tests/test_cursor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 1.2.5
3
+ Version: 1.4.0
4
4
  Summary: The DynamoDB issuance a GraphDDB behavior module binds: the leaf transports, table mapping, AttributeValue codec and cursor codecs.
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -48,6 +48,19 @@ def _key_attrs(index_name: str) -> tuple:
48
48
  return (f"{index_name}PK", f"{index_name}SK") if index_name else ("PK", "SK")
49
49
 
50
50
 
51
+ #: The sort-key predicate of each ``rangeOperator`` ``queryItems`` issues on ``key.sk``
52
+ #: (``between`` reaches up to ``rangeUpper``).
53
+ _SORT_KEY_CLAUSE = {
54
+ "eq": "#ks = :ks",
55
+ "begins_with": "begins_with(#ks, :ks)",
56
+ "lt": "#ks < :ks",
57
+ "le": "#ks <= :ks",
58
+ "gt": "#ks > :ks",
59
+ "ge": "#ks >= :ks",
60
+ "between": "#ks BETWEEN :ks AND :ku",
61
+ }
62
+
63
+
51
64
  def _projection_request(projection: List[str]) -> Dict[str, Any]:
52
65
  """``ProjectionExpression`` + names for the declared projection.
53
66
 
@@ -107,20 +120,30 @@ def leaf_handlers(runtime: Any) -> Dict[str, Callable[..., Mapping[str, Any]]]:
107
120
  index_name = ports["indexName"]
108
121
  pk_attr, sk_attr = _key_attrs(index_name)
109
122
  operator = ports["rangeOperator"]
110
- if operator == "begins_with":
111
- sk_clause = "begins_with(#ks, :ks)"
112
- elif operator == "eq":
113
- sk_clause = "#ks = :ks"
114
- else:
115
- raise GraphDDBError(f"queryItems: unsupported range operator '{operator}'")
123
+ sk_clause = _SORT_KEY_CLAUSE.get(operator)
124
+ if sk_clause is None:
125
+ raise GraphDDBError(
126
+ f"queryItems: unsupported range operator '{operator}' "
127
+ f"(expected {', '.join(_SORT_KEY_CLAUSE)})."
128
+ )
129
+ upper = ports["rangeUpper"]
130
+ if operator != "between" and upper != "":
131
+ raise GraphDDBError(
132
+ "queryItems: rangeUpper is the upper operand of 'between'; "
133
+ f"operator '{operator}' takes none."
134
+ )
135
+ values = {
136
+ ":kp": runtime._serializer.serialize(ports["key"]["pk"]),
137
+ ":ks": runtime._serializer.serialize(ports["key"]["sk"]),
138
+ }
139
+ if operator == "between":
140
+ values[":ku"] = runtime._serializer.serialize(upper)
116
141
  request: Dict[str, Any] = {
117
142
  "TableName": runtime._physical_table(ports["table"]),
118
143
  "KeyConditionExpression": f"#kp = :kp AND {sk_clause}",
119
144
  "ExpressionAttributeNames": {"#kp": pk_attr, "#ks": sk_attr},
120
- "ExpressionAttributeValues": {
121
- ":kp": runtime._serializer.serialize(ports["key"]["pk"]),
122
- ":ks": runtime._serializer.serialize(ports["key"]["sk"]),
123
- },
145
+ "ExpressionAttributeValues": values,
146
+ "ScanIndexForward": ports["scanIndexForward"],
124
147
  "Limit": ports["limit"],
125
148
  }
126
149
  projection = _projection_request(list(ports["projectionFields"]))
@@ -197,7 +220,7 @@ def leaf_handlers(runtime: Any) -> Dict[str, Callable[..., Mapping[str, Any]]]:
197
220
  # A condition miss is an EXPECTED OUTCOME of a conditional write, not a
198
221
  # transport failure, so a write handler returns ``{"ok": applied}`` rather
199
222
  # than raising — the behavior branches on it (the two-stage atomic ADD).
200
- # This mirrors ``src/leaf/ddb.ts`` ``isConditionMiss``; the CCF detector is
223
+ # This mirrors the write leaves of ``src/leaf/ddb.ts``; the CCF detector is
201
224
  # the runtime's single one (``_is_conditional_check_failure``).
202
225
 
203
226
  def _expr_maps(ports: Mapping[str, Any], request: Dict[str, Any]) -> None:
@@ -51,9 +51,8 @@ def _is_conditional_check_failure(err: BaseException) -> bool:
51
51
  ``original`` is the boto ``ClientError`` (its ``response.Error.Code`` is
52
52
  ``ConditionalCheckFailedException``). A fake test client may raise a plain
53
53
  exception standing in for the boto error; matched by the exception name / its
54
- string carrying ``ConditionalCheckFailed`` (mirroring the TS ``isConditionMiss``,
55
- ``src/leaf/ddb.ts``). Unwraps ``__cause__`` / ``original`` so a wrapped error is
56
- detected identically."""
54
+ string carrying ``ConditionalCheckFailed``. Unwraps ``__cause__`` / ``original``
55
+ so a wrapped error is detected identically."""
57
56
  seen: set = set()
58
57
  stack: List[BaseException] = [err]
59
58
  while stack:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 1.2.5
3
+ Version: 1.4.0
4
4
  Summary: The DynamoDB issuance a GraphDDB behavior module binds: the leaf transports, table mapping, AttributeValue codec and cursor codecs.
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphddb-runtime"
7
- version = "1.2.5"
7
+ version = "1.4.0"
8
8
  description = "The DynamoDB issuance a GraphDDB behavior module binds: the leaf transports, table mapping, AttributeValue codec and cursor codecs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -37,6 +37,7 @@ import pytest
37
37
  from boto3.dynamodb.types import TypeDeserializer
38
38
 
39
39
  from graphddb_runtime import (
40
+ GraphDDBError,
40
41
  GraphDDBRuntime,
41
42
  Guard,
42
43
  OperationExecutionError,
@@ -181,6 +182,8 @@ def test_binds_every_component_the_corpus_declares(generated_module):
181
182
  "bulkAdd",
182
183
  "bulkRemove",
183
184
  "byGroup",
185
+ "latestBefore",
186
+ "joinedSince",
184
187
  "pair",
185
188
  "create",
186
189
  "bumpRole",
@@ -285,6 +288,67 @@ def test_query_items_eq_through_a_gsi_keys_on_the_index_attributes(generated_mod
285
288
  }
286
289
 
287
290
 
291
+ def test_query_items_between_reads_descending(generated_module):
292
+ client = RecordingClient(responses={"query": {"Items": []}})
293
+ bound = _bind(generated_module, client)
294
+
295
+ assert bound["latestBefore"]({"userId": "alice", "kind": "JOIN", "day": "2026-10-01"}) == []
296
+
297
+ (request,) = client.requests("query")
298
+ # `between` reaches from `key.sk` up to `rangeUpper`, inclusive.
299
+ assert request["KeyConditionExpression"] == "#kp = :kp AND #ks BETWEEN :ks AND :ku"
300
+ assert _plain(request["ExpressionAttributeValues"]) == {
301
+ ":kp": "USER#alice",
302
+ ":ks": "JOIN#",
303
+ ":ku": "JOIN#2026-10-01",
304
+ }
305
+ assert request["ScanIndexForward"] is False
306
+ assert request["Limit"] == 1
307
+
308
+
309
+ def test_query_items_compares_a_gsi_sort_key(generated_module):
310
+ client = RecordingClient(responses={"query": {"Items": []}})
311
+ bound = _bind(generated_module, client)
312
+
313
+ assert bound["joinedSince"]({"groupId": "g1", "since": "2026-10-01", "limit": 5}) == []
314
+
315
+ (request,) = client.requests("query")
316
+ assert request["IndexName"] == "GSI1"
317
+ assert request["KeyConditionExpression"] == "#kp = :kp AND #ks >= :ks"
318
+ assert request["ExpressionAttributeNames"]["#ks"] == "GSI1SK"
319
+ assert _plain(request["ExpressionAttributeValues"]) == {
320
+ ":kp": "GROUP#g1",
321
+ ":ks": "JOINED#2026-10-01",
322
+ }
323
+ assert request["ScanIndexForward"] is True
324
+
325
+
326
+ def _query_ports(operator, upper=""):
327
+ return {
328
+ "table": "members",
329
+ "indexName": "",
330
+ "key": {"pk": "USER#alice", "sk": "GROUP#"},
331
+ "cursor": "",
332
+ "rangeOperator": operator,
333
+ "rangeUpper": upper,
334
+ "scanIndexForward": True,
335
+ "limit": 10,
336
+ "projectionFields": [],
337
+ }
338
+
339
+
340
+ def test_query_items_refuses_an_operator_it_cannot_issue():
341
+ handlers = _handlers(RecordingClient())
342
+ with pytest.raises(GraphDDBError, match="unsupported range operator 'contains'"):
343
+ handlers["queryItems"](_query_ports("contains"), {"component": "queryItems"})
344
+
345
+
346
+ def test_query_items_refuses_an_upper_operand_without_between():
347
+ handlers = _handlers(RecordingClient())
348
+ with pytest.raises(GraphDDBError, match="rangeUpper is the upper operand of 'between'"):
349
+ handlers["queryItems"](_query_ports("lt", "GROUP#z"), {"component": "queryItems"})
350
+
351
+
288
352
  def test_batch_get_items_requests_every_key_in_one_batch(generated_module):
289
353
  # A projected read returns the projected fields — which is exactly what the
290
354
  # behavior declares its rows to be (`MemberRef`, not the whole `MemberRow`).