graphddb-runtime 1.3.0__tar.gz → 1.5.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.3.0 → graphddb_runtime-1.5.0}/PKG-INFO +1 -1
  2. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/leaf_handlers.py +33 -10
  3. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime.egg-info/PKG-INFO +1 -1
  4. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/pyproject.toml +1 -1
  5. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/tests/test_leaf_handlers.py +64 -0
  6. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/README.md +0 -0
  7. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/__init__.py +0 -0
  8. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/batch.py +0 -0
  9. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/cursor.py +0 -0
  10. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/errors.py +0 -0
  11. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/limits.py +0 -0
  12. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/per_key_cursor.py +0 -0
  13. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/runtime.py +0 -0
  14. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime/transport.py +0 -0
  15. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
  16. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
  17. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime.egg-info/requires.txt +0 -0
  18. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/graphddb_runtime.egg-info/top_level.txt +0 -0
  19. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/setup.cfg +0 -0
  20. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/tests/test_crosslang_graphddb_cell.py +0 -0
  21. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.0}/tests/test_crosslang_python_cells.py +0 -0
  22. {graphddb_runtime-1.3.0 → graphddb_runtime-1.5.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.3.0
3
+ Version: 1.5.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"]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 1.3.0
3
+ Version: 1.5.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.3.0"
7
+ version = "1.5.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`).