graphddb-runtime 1.1.0__tar.gz → 1.2.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 (69) hide show
  1. graphddb_runtime-1.2.0/PKG-INFO +136 -0
  2. graphddb_runtime-1.2.0/README.md +124 -0
  3. graphddb_runtime-1.2.0/graphddb_runtime/__init__.py +58 -0
  4. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/graphddb_runtime/batch.py +5 -15
  5. graphddb_runtime-1.2.0/graphddb_runtime/errors.py +24 -0
  6. graphddb_runtime-1.2.0/graphddb_runtime/leaf_handlers.py +340 -0
  7. graphddb_runtime-1.2.0/graphddb_runtime/limits.py +29 -0
  8. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/graphddb_runtime/per_key_cursor.py +4 -6
  9. graphddb_runtime-1.2.0/graphddb_runtime/runtime.py +256 -0
  10. graphddb_runtime-1.2.0/graphddb_runtime/transport.py +133 -0
  11. graphddb_runtime-1.2.0/graphddb_runtime.egg-info/PKG-INFO +136 -0
  12. graphddb_runtime-1.2.0/graphddb_runtime.egg-info/SOURCES.txt +20 -0
  13. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/graphddb_runtime.egg-info/requires.txt +1 -1
  14. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/pyproject.toml +3 -6
  15. graphddb_runtime-1.2.0/tests/test_crosslang_graphddb_cell.py +891 -0
  16. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/tests/test_crosslang_python_cells.py +9 -184
  17. graphddb_runtime-1.2.0/tests/test_cursor.py +61 -0
  18. graphddb_runtime-1.2.0/tests/test_leaf_handlers.py +925 -0
  19. graphddb_runtime-1.1.0/PKG-INFO +0 -161
  20. graphddb_runtime-1.1.0/README.md +0 -149
  21. graphddb_runtime-1.1.0/graphddb_runtime/__init__.py +0 -78
  22. graphddb_runtime-1.1.0/graphddb_runtime/async_runtime.py +0 -134
  23. graphddb_runtime-1.1.0/graphddb_runtime/behavior_exec.py +0 -1074
  24. graphddb_runtime-1.1.0/graphddb_runtime/concurrency.py +0 -87
  25. graphddb_runtime-1.1.0/graphddb_runtime/errors.py +0 -80
  26. graphddb_runtime-1.1.0/graphddb_runtime/filters.py +0 -208
  27. graphddb_runtime-1.1.0/graphddb_runtime/handler_registry.py +0 -170
  28. graphddb_runtime-1.1.0/graphddb_runtime/hydration.py +0 -75
  29. graphddb_runtime-1.1.0/graphddb_runtime/limits.py +0 -20
  30. graphddb_runtime-1.1.0/graphddb_runtime/middleware.py +0 -485
  31. graphddb_runtime-1.1.0/graphddb_runtime/runtime.py +0 -2603
  32. graphddb_runtime-1.1.0/graphddb_runtime/templates.py +0 -320
  33. graphddb_runtime-1.1.0/graphddb_runtime/transactions.py +0 -595
  34. graphddb_runtime-1.1.0/graphddb_runtime.egg-info/PKG-INFO +0 -161
  35. graphddb_runtime-1.1.0/graphddb_runtime.egg-info/SOURCES.txt +0 -54
  36. graphddb_runtime-1.1.0/tests/test_atomic_add_command.py +0 -200
  37. graphddb_runtime-1.1.0/tests/test_concurrency.py +0 -357
  38. graphddb_runtime-1.1.0/tests/test_contract_runtime.py +0 -427
  39. graphddb_runtime-1.1.0/tests/test_description.py +0 -78
  40. graphddb_runtime-1.1.0/tests/test_description_followups.py +0 -65
  41. graphddb_runtime-1.1.0/tests/test_description_jsdoc.py +0 -99
  42. graphddb_runtime-1.1.0/tests/test_error_policy.py +0 -183
  43. graphddb_runtime-1.1.0/tests/test_generated_module.py +0 -293
  44. graphddb_runtime-1.1.0/tests/test_guard_eval.py +0 -37
  45. graphddb_runtime-1.1.0/tests/test_handler_registry.py +0 -72
  46. graphddb_runtime-1.1.0/tests/test_integration.py +0 -456
  47. graphddb_runtime-1.1.0/tests/test_integration_batch_write.py +0 -263
  48. graphddb_runtime-1.1.0/tests/test_integration_binding_contract.py +0 -426
  49. graphddb_runtime-1.1.0/tests/test_integration_command.py +0 -333
  50. graphddb_runtime-1.1.0/tests/test_integration_compose.py +0 -186
  51. graphddb_runtime-1.1.0/tests/test_integration_contract.py +0 -374
  52. graphddb_runtime-1.1.0/tests/test_integration_edge_derive.py +0 -238
  53. graphddb_runtime-1.1.0/tests/test_integration_edge_write.py +0 -206
  54. graphddb_runtime-1.1.0/tests/test_integration_events.py +0 -199
  55. graphddb_runtime-1.1.0/tests/test_integration_maintain.py +0 -337
  56. graphddb_runtime-1.1.0/tests/test_integration_middleware.py +0 -312
  57. graphddb_runtime-1.1.0/tests/test_integration_referential.py +0 -156
  58. graphddb_runtime-1.1.0/tests/test_integration_relations.py +0 -377
  59. graphddb_runtime-1.1.0/tests/test_integration_unique.py +0 -246
  60. graphddb_runtime-1.1.0/tests/test_maintain.py +0 -280
  61. graphddb_runtime-1.1.0/tests/test_middleware.py +0 -1014
  62. graphddb_runtime-1.1.0/tests/test_relations.py +0 -383
  63. graphddb_runtime-1.1.0/tests/test_spec_version_guard.py +0 -159
  64. graphddb_runtime-1.1.0/tests/test_ttl.py +0 -57
  65. graphddb_runtime-1.1.0/tests/test_unit.py +0 -1332
  66. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/graphddb_runtime/cursor.py +0 -0
  67. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
  68. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/graphddb_runtime.egg-info/top_level.txt +0 -0
  69. {graphddb_runtime-1.1.0 → graphddb_runtime-1.2.0}/setup.cfg +0 -0
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: graphddb-runtime
3
+ Version: 1.2.0
4
+ Summary: The DynamoDB issuance a GraphDDB behavior module binds: the leaf transports, table mapping, AttributeValue codec and cursor codecs.
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: boto3>=1.26
9
+ Requires-Dist: behavior-contracts==0.11.16
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest>=7.0; extra == "test"
12
+
13
+ # graphddb-runtime
14
+
15
+ The DynamoDB issuance a [GraphDDB](https://www.npmjs.com/package/graphddb) behavior
16
+ module binds, for Python.
17
+
18
+ An operation is **behavior**: you author it as a `@behavior static` method over
19
+ graphddb's `@leaf` catalog in ordinary TypeScript, `bc generate --lang python` emits
20
+ the module that runs it, and `graphddb-runtime` supplies what one physical DynamoDB
21
+ request needs and a behavior cannot carry — the boto3 client, the logical → physical
22
+ table mapping, the AttributeValue codec, the execution bounds and the error mapping.
23
+
24
+ It interprets no specification document. Code generation and IR belong to
25
+ [behavior-contracts](https://pypi.org/project/behavior-contracts/); the generated
26
+ module embeds its own IR and gates its own spec version when imported.
27
+
28
+ ## Features
29
+
30
+ - **Leaf transports** — `leaf_handlers(runtime)` supplies the `GetItem` / `Query` /
31
+ `BatchGetItem` / `PutItem` / `UpdateItem` / `DeleteItem` issuance a compiled
32
+ behavior binds. Each handler issues ONE physical request and returns the raw
33
+ row(s): no ordering, no fan-out, no key derivation, no result shaping — those are
34
+ the behavior's, compiled by behavior-contracts.
35
+ - **Cursor codecs** — the opaque base64url pagination cursor, byte-identical to the
36
+ TypeScript and PHP runtimes'.
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install graphddb-runtime
42
+ ```
43
+
44
+ Requires Python 3.9+ and boto3.
45
+
46
+ > **Versioning.** `graphddb-runtime` tracks the `graphddb` npm package version: a
47
+ > given runtime release matches the `graphddb` CLI of the same version.
48
+
49
+ ## Usage
50
+
51
+ Construct the runtime with a boto3 client, then hand its leaf handlers to the
52
+ generated module's `bind`:
53
+
54
+ ```python
55
+ import boto3
56
+ import my_behaviors as gen # bc generate --lang python --from behaviors.ts
57
+ from graphddb_runtime import GraphDDBRuntime, leaf_handlers
58
+
59
+ runtime = GraphDDBRuntime(
60
+ dynamodb_client=boto3.client("dynamodb"),
61
+ # Map logical table names to deployed physical names when they differ.
62
+ table_mapping={"UserPermissions": "UserPermissions-prod"},
63
+ )
64
+ bound = gen.bind(leaf_handlers(runtime))
65
+
66
+ user = bound["getUserByEmail"]({"email": "alice@example.com"})
67
+ groups = bound["listUserGroups"]({"userId": "alice", "limit": 20})
68
+ ```
69
+
70
+ A write behavior returns whether it applied — a DynamoDB
71
+ `ConditionalCheckFailedException` is an expected outcome of a conditional write, so
72
+ it comes back as `False` rather than raising, and the behavior branches on it:
73
+
74
+ ```python
75
+ applied = bound["createMembership"]({"userId": "alice", "groupId": "eng", "role": "admin"})
76
+ ```
77
+
78
+ ## AWS Lambda
79
+
80
+ Constructing the boto3 client and importing the generated module are both cold-start
81
+ costs you want to pay **once**, in module scope, so they are reused across warm
82
+ invocations (and frozen by SnapStart).
83
+
84
+ ```python
85
+ # handler.py — module scope runs once per execution environment (cold start).
86
+ import json
87
+ import boto3
88
+ import my_behaviors as gen
89
+ from graphddb_runtime import GraphDDBRuntime, leaf_handlers
90
+
91
+ _bound = gen.bind(
92
+ leaf_handlers(
93
+ GraphDDBRuntime(
94
+ dynamodb_client=boto3.client("dynamodb"),
95
+ table_mapping={"UserPermissions": "UserPermissions-prod"},
96
+ )
97
+ )
98
+ )
99
+
100
+
101
+ def handler(event, context):
102
+ user = _bound["getUserByEmail"]({"email": event["queryStringParameters"]["email"]})
103
+ if user is None:
104
+ return {"statusCode": 404, "body": "not found"}
105
+ return {"statusCode": 200, "body": json.dumps(user)}
106
+ ```
107
+
108
+ ### SnapStart
109
+
110
+ Lambda SnapStart snapshots the initialized execution environment after the
111
+ module-scope code runs, so the client, the imported module and the `bind` are
112
+ captured in the snapshot and skipped on restore.
113
+
114
+ - **Bind in module scope** (as above), never inside the handler — that is what gets
115
+ snapshotted.
116
+ - **Do not cache short-lived state across the snapshot** (credentials/tokens with an
117
+ expiry, random seeds). The DynamoDB client and the embedded IR are safe to
118
+ snapshot; refresh anything time-sensitive inside the handler.
119
+
120
+ ### Packaging
121
+
122
+ The deployment artifact needs three things: this runtime package, the
123
+ `behavior-contracts` runtime the generated module imports, and the generated module
124
+ itself. boto3/botocore are provided by the Lambda Python runtime, so they need not be
125
+ vendored (pin them only if you require a specific version).
126
+
127
+ ```bash
128
+ mkdir -p build
129
+ pip install graphddb-runtime --target build # pulls in behavior-contracts
130
+ cp my_behaviors.py handler.py build/
131
+ ( cd build && zip -r ../function.zip . ) # handler = handler.handler
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,124 @@
1
+ # graphddb-runtime
2
+
3
+ The DynamoDB issuance a [GraphDDB](https://www.npmjs.com/package/graphddb) behavior
4
+ module binds, for Python.
5
+
6
+ An operation is **behavior**: you author it as a `@behavior static` method over
7
+ graphddb's `@leaf` catalog in ordinary TypeScript, `bc generate --lang python` emits
8
+ the module that runs it, and `graphddb-runtime` supplies what one physical DynamoDB
9
+ request needs and a behavior cannot carry — the boto3 client, the logical → physical
10
+ table mapping, the AttributeValue codec, the execution bounds and the error mapping.
11
+
12
+ It interprets no specification document. Code generation and IR belong to
13
+ [behavior-contracts](https://pypi.org/project/behavior-contracts/); the generated
14
+ module embeds its own IR and gates its own spec version when imported.
15
+
16
+ ## Features
17
+
18
+ - **Leaf transports** — `leaf_handlers(runtime)` supplies the `GetItem` / `Query` /
19
+ `BatchGetItem` / `PutItem` / `UpdateItem` / `DeleteItem` issuance a compiled
20
+ behavior binds. Each handler issues ONE physical request and returns the raw
21
+ row(s): no ordering, no fan-out, no key derivation, no result shaping — those are
22
+ the behavior's, compiled by behavior-contracts.
23
+ - **Cursor codecs** — the opaque base64url pagination cursor, byte-identical to the
24
+ TypeScript and PHP runtimes'.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install graphddb-runtime
30
+ ```
31
+
32
+ Requires Python 3.9+ and boto3.
33
+
34
+ > **Versioning.** `graphddb-runtime` tracks the `graphddb` npm package version: a
35
+ > given runtime release matches the `graphddb` CLI of the same version.
36
+
37
+ ## Usage
38
+
39
+ Construct the runtime with a boto3 client, then hand its leaf handlers to the
40
+ generated module's `bind`:
41
+
42
+ ```python
43
+ import boto3
44
+ import my_behaviors as gen # bc generate --lang python --from behaviors.ts
45
+ from graphddb_runtime import GraphDDBRuntime, leaf_handlers
46
+
47
+ runtime = GraphDDBRuntime(
48
+ dynamodb_client=boto3.client("dynamodb"),
49
+ # Map logical table names to deployed physical names when they differ.
50
+ table_mapping={"UserPermissions": "UserPermissions-prod"},
51
+ )
52
+ bound = gen.bind(leaf_handlers(runtime))
53
+
54
+ user = bound["getUserByEmail"]({"email": "alice@example.com"})
55
+ groups = bound["listUserGroups"]({"userId": "alice", "limit": 20})
56
+ ```
57
+
58
+ A write behavior returns whether it applied — a DynamoDB
59
+ `ConditionalCheckFailedException` is an expected outcome of a conditional write, so
60
+ it comes back as `False` rather than raising, and the behavior branches on it:
61
+
62
+ ```python
63
+ applied = bound["createMembership"]({"userId": "alice", "groupId": "eng", "role": "admin"})
64
+ ```
65
+
66
+ ## AWS Lambda
67
+
68
+ Constructing the boto3 client and importing the generated module are both cold-start
69
+ costs you want to pay **once**, in module scope, so they are reused across warm
70
+ invocations (and frozen by SnapStart).
71
+
72
+ ```python
73
+ # handler.py — module scope runs once per execution environment (cold start).
74
+ import json
75
+ import boto3
76
+ import my_behaviors as gen
77
+ from graphddb_runtime import GraphDDBRuntime, leaf_handlers
78
+
79
+ _bound = gen.bind(
80
+ leaf_handlers(
81
+ GraphDDBRuntime(
82
+ dynamodb_client=boto3.client("dynamodb"),
83
+ table_mapping={"UserPermissions": "UserPermissions-prod"},
84
+ )
85
+ )
86
+ )
87
+
88
+
89
+ def handler(event, context):
90
+ user = _bound["getUserByEmail"]({"email": event["queryStringParameters"]["email"]})
91
+ if user is None:
92
+ return {"statusCode": 404, "body": "not found"}
93
+ return {"statusCode": 200, "body": json.dumps(user)}
94
+ ```
95
+
96
+ ### SnapStart
97
+
98
+ Lambda SnapStart snapshots the initialized execution environment after the
99
+ module-scope code runs, so the client, the imported module and the `bind` are
100
+ captured in the snapshot and skipped on restore.
101
+
102
+ - **Bind in module scope** (as above), never inside the handler — that is what gets
103
+ snapshotted.
104
+ - **Do not cache short-lived state across the snapshot** (credentials/tokens with an
105
+ expiry, random seeds). The DynamoDB client and the embedded IR are safe to
106
+ snapshot; refresh anything time-sensitive inside the handler.
107
+
108
+ ### Packaging
109
+
110
+ The deployment artifact needs three things: this runtime package, the
111
+ `behavior-contracts` runtime the generated module imports, and the generated module
112
+ itself. boto3/botocore are provided by the Lambda Python runtime, so they need not be
113
+ vendored (pin them only if you require a specific version).
114
+
115
+ ```bash
116
+ mkdir -p build
117
+ pip install graphddb-runtime --target build # pulls in behavior-contracts
118
+ cp my_behaviors.py handler.py build/
119
+ ( cd build && zip -r ../function.zip . ) # handler = handler.handler
120
+ ```
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,58 @@
1
+ """GraphDDB Python runtime — the DynamoDB issuance a generated module binds.
2
+
3
+ An operation is behavior: the consumer authors it as a ``@behavior`` method over
4
+ graphddb's ``@leaf`` catalog, ``bc generate`` emits the module, and this package
5
+ supplies what one physical request needs::
6
+
7
+ import my_behaviors as gen
8
+ from graphddb_runtime import GraphDDBRuntime, leaf_handlers
9
+
10
+ runtime = GraphDDBRuntime(boto3.client("dynamodb"), {"members": "AppTable"})
11
+ bound = gen.bind(leaf_handlers(runtime))
12
+ page = bound["listUserGroups"]({"userId": "alice"})
13
+
14
+ Public surface:
15
+
16
+ - :class:`GraphDDBRuntime` — the issuance context (client, table mapping, codec,
17
+ bounds, error mapping).
18
+ - :func:`leaf_handlers` — graphddb's DynamoDB leaf transports, for ``bind``.
19
+ - :class:`RuntimeLimits` — execution-time bounds.
20
+ - the :class:`GraphDDBError` family.
21
+ - the cursor codecs, for a paged read's opaque cursor.
22
+ - the transport-middleware vocabulary (:class:`RequestCtx` / :class:`Outcome` /
23
+ :class:`Refuse` / :class:`Guard` / :class:`Respond`), for a host that wraps the
24
+ physical requests — see :mod:`graphddb_runtime.transport`.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from .cursor import decode_cursor, encode_cursor
30
+ from .errors import GraphDDBError, OperationExecutionError
31
+ from .limits import RuntimeLimits
32
+ from .per_key_cursor import (
33
+ decode_per_key_cursor,
34
+ encode_per_key_cursor,
35
+ serialize_contract_key,
36
+ )
37
+ from .runtime import GraphDDBRuntime
38
+ from .leaf_handlers import leaf_handlers
39
+ from .transport import Guard, Outcome, Refuse, RequestCtx, Respond
40
+
41
+ __all__ = [
42
+ "GraphDDBRuntime",
43
+ "leaf_handlers",
44
+ "RuntimeLimits",
45
+ # transport middleware — the host's seam around one physical request
46
+ "RequestCtx",
47
+ "Outcome",
48
+ "Refuse",
49
+ "Guard",
50
+ "Respond",
51
+ "GraphDDBError",
52
+ "OperationExecutionError",
53
+ "encode_cursor",
54
+ "decode_cursor",
55
+ "serialize_contract_key",
56
+ "encode_per_key_cursor",
57
+ "decode_per_key_cursor",
58
+ ]
@@ -3,8 +3,9 @@
3
3
  Port of the TypeScript batch semantics (``src/operations/batch-retry.ts`` /
4
4
  ``src/executor/batch-executor.ts``):
5
5
 
6
- - keys are **deduped** before the request (``planBatchGetForQueryKeys`` /
7
- ``dedupeDynamoKeys``);
6
+ - keys arrive already **unique**: removing duplicates is the leaf transport's,
7
+ upstream of chunking (:func:`graphddb_runtime.leaf_handlers.leaf_handlers`
8
+ ``batch_get_items``);
8
9
  - split into chunks of at most :data:`BATCH_GET_MAX_KEYS` (100) keys;
9
10
  - each chunk retries ``UnprocessedKeys`` with exponential backoff
10
11
  (``50 * 2^(attempt-1)`` ms, capped at 1000ms) up to
@@ -17,7 +18,7 @@ real delays.
17
18
  from __future__ import annotations
18
19
 
19
20
  import time
20
- from typing import Any, Callable, Dict, List, Mapping, Optional
21
+ from typing import Any, Callable, Dict, List, Optional
21
22
 
22
23
  # Mirrors src/operations/batch-retry.ts.
23
24
  BATCH_GET_MAX_KEYS = 100
@@ -34,19 +35,8 @@ def chunk(items: List[Any], size: int) -> List[List[Any]]:
34
35
  return [items[i : i + size] for i in range(0, len(items), size)]
35
36
 
36
37
 
37
- def serialize_key(key: Mapping[str, Any]) -> str:
38
- """Stable string form of a (plain) key for dedup / parent matching.
39
-
40
- Mirrors the TS ``dedupeDynamoKeys`` / ``serializeQueryKey`` ordering
41
- (attributes sorted by name).
42
- """
43
- import json
44
-
45
- return json.dumps(sorted(key.items()), separators=(",", ":"), default=str)
46
-
47
-
48
38
  class BatchGetExecutor:
49
- """Executes a deduped, chunked, retrying BatchGetItem against a boto3 client.
39
+ """Executes a chunked, retrying BatchGetItem against a boto3 client.
50
40
 
51
41
  The client is the low-level ``boto3.client("dynamodb")`` (AttributeValue
52
42
  shapes). ``request_extra`` carries the optional ``ProjectionExpression`` /
@@ -0,0 +1,24 @@
1
+ """Error hierarchy for the GraphDDB Python runtime.
2
+
3
+ Every runtime-raised error derives from :class:`GraphDDBError` so callers can catch
4
+ the whole family with a single ``except``. Only errors this runtime actually raises
5
+ live here — a declared-but-never-raised class reads as a handled condition that is
6
+ in fact unreachable.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ class GraphDDBError(Exception):
13
+ """Base class for every error raised by the runtime."""
14
+
15
+
16
+ class OperationExecutionError(GraphDDBError):
17
+ """Wraps a boto3 ``ClientError`` (or any boto3 failure) from a DynamoDB call.
18
+
19
+ The original exception is preserved as ``__cause__`` and on ``original``.
20
+ """
21
+
22
+ def __init__(self, message: str, original: BaseException | None = None) -> None:
23
+ super().__init__(message)
24
+ self.original = original