graphddb-runtime 0.7.4__tar.gz → 0.7.6__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 (48) hide show
  1. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/PKG-INFO +1 -1
  2. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/runtime.py +25 -0
  3. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime.egg-info/PKG-INFO +1 -1
  4. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime.egg-info/SOURCES.txt +1 -0
  5. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/pyproject.toml +1 -1
  6. graphddb_runtime-0.7.6/tests/test_crosslang_python_cells.py +408 -0
  7. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/README.md +0 -0
  8. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/__init__.py +0 -0
  9. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/async_runtime.py +0 -0
  10. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/batch.py +0 -0
  11. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/concurrency.py +0 -0
  12. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/cursor.py +0 -0
  13. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/errors.py +0 -0
  14. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/filters.py +0 -0
  15. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/hydration.py +0 -0
  16. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/limits.py +0 -0
  17. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/middleware.py +0 -0
  18. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/per_key_cursor.py +0 -0
  19. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/relations.py +0 -0
  20. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/templates.py +0 -0
  21. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime/transactions.py +0 -0
  22. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
  23. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime.egg-info/requires.txt +0 -0
  24. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/graphddb_runtime.egg-info/top_level.txt +0 -0
  25. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/setup.cfg +0 -0
  26. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_concurrency.py +0 -0
  27. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_contract_runtime.py +0 -0
  28. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_description.py +0 -0
  29. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_description_followups.py +0 -0
  30. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_description_jsdoc.py +0 -0
  31. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration.py +0 -0
  32. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_command.py +0 -0
  33. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_compose.py +0 -0
  34. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_contract.py +0 -0
  35. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_edge_derive.py +0 -0
  36. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_edge_write.py +0 -0
  37. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_events.py +0 -0
  38. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_maintain.py +0 -0
  39. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_middleware.py +0 -0
  40. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_referential.py +0 -0
  41. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_relations.py +0 -0
  42. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_integration_unique.py +0 -0
  43. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_maintain.py +0 -0
  44. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_middleware.py +0 -0
  45. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_relations.py +0 -0
  46. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_spec_version_guard.py +0 -0
  47. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_ttl.py +0 -0
  48. {graphddb_runtime-0.7.4 → graphddb_runtime-0.7.6}/tests/test_unit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Thin DynamoDB executor for GraphDDB-generated Python repositories (single-operation core, issue #44).
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -2137,6 +2137,19 @@ class GraphDDBRuntime:
2137
2137
  "TableName": self._physical_table(op["tableName"]),
2138
2138
  "Key": key,
2139
2139
  }
2140
+ # Server-side projection (issue #237): push the `select` down as a
2141
+ # ``ProjectionExpression`` so DynamoDB returns only the requested
2142
+ # attributes (+ key attrs) instead of the full item — mirroring the TS
2143
+ # runtime (``src/executor/dynamo-executor.ts`` ``executeGetItem``) and the
2144
+ # BatchGet leg's ``_projection_request``. Hydration then runs over the
2145
+ # projected subset (still byte-identical, since every attr hydration /
2146
+ # relations need is in ``op["projection"]`` + ``keyCondition``).
2147
+ projection = self._projection_request(op, select)
2148
+ if projection:
2149
+ request["ProjectionExpression"] = projection["ProjectionExpression"]
2150
+ request["ExpressionAttributeNames"] = projection[
2151
+ "ExpressionAttributeNames"
2152
+ ]
2140
2153
  # Strongly-consistent point read on the base table (issue #65). The TS
2141
2154
  # runtime sets ``ConsistentRead`` on a base-table GetItem when the contract
2142
2155
  # method's ``consistentRead`` retrieval option is truthy
@@ -2211,6 +2224,18 @@ class GraphDDBRuntime:
2211
2224
  if op.get("indexName"):
2212
2225
  request["IndexName"] = op["indexName"]
2213
2226
 
2227
+ # Server-side projection (issue #237): push the `select` down as a
2228
+ # ``ProjectionExpression`` (mirroring TS ``executeQuery`` and the BatchGet
2229
+ # ``_projection_request``). Projection name aliases (``#p*``) are merged
2230
+ # into the shared ``ExpressionAttributeNames`` — they never collide with
2231
+ # the key-condition (``#k*`` / ``#kr``) or filter aliases. DynamoDB still
2232
+ # returns the full ``LastEvaluatedKey`` regardless of projection, so
2233
+ # cursor continuation is unaffected.
2234
+ projection = self._projection_request(op, select)
2235
+ if projection:
2236
+ request["ProjectionExpression"] = projection["ProjectionExpression"]
2237
+ names.update(projection["ExpressionAttributeNames"])
2238
+
2214
2239
  # Strongly-consistent read on a base-table Query (issue #65). The TS
2215
2240
  # planner (``src/planner/planner.ts``) sets ``ConsistentRead`` only for a
2216
2241
  # primary-key (base-table) query, so we mirror that for the valid case:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphddb-runtime
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Thin DynamoDB executor for GraphDDB-generated Python repositories (single-operation core, issue #44).
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -22,6 +22,7 @@ graphddb_runtime.egg-info/requires.txt
22
22
  graphddb_runtime.egg-info/top_level.txt
23
23
  tests/test_concurrency.py
24
24
  tests/test_contract_runtime.py
25
+ tests/test_crosslang_python_cells.py
25
26
  tests/test_description.py
26
27
  tests/test_description_followups.py
27
28
  tests/test_description_jsdoc.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "graphddb-runtime"
7
- version = "0.7.4"
7
+ version = "0.7.6"
8
8
  description = "Thin DynamoDB executor for GraphDDB-generated Python repositories (single-operation core, issue #44)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,408 @@
1
+ """Unit tests for the Python cross-language benchmark cells (issue #232).
2
+
3
+ These are NON-integration (no DynamoDB Local): they drive the two cells
4
+ (boto3 sdk baseline + graphddb_runtime) against a botocore ``Stubber`` (canned
5
+ responses, request-shape asserted) or the micro mock transport, so they run in
6
+ ``npm run test:py``. They prove:
7
+
8
+ 1. the boto3 SDK baseline issues the right requests and hydrates correctly, and
9
+ reports the expected requests/op + items/op (the fairness-gate cost);
10
+ 2. the graphddb_runtime cell (over the generated IR) returns correct data and
11
+ reports the SAME req/op + items/op as the baseline (cross-language fairness);
12
+ 3. the micro mock genuinely EXCLUDES I/O — the full boto3 client stack runs
13
+ (marshal + parse + hydrate) with NO socket opened.
14
+
15
+ The cells live under ``benchmark/crosslang/adapters/python`` (outside the pytest
16
+ rootdir), so their directory is added to ``sys.path`` here.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import socket
23
+ import sys
24
+
25
+ import boto3
26
+ import pytest
27
+ from botocore.config import Config
28
+ from botocore.stub import Stubber
29
+
30
+ # Make the benchmark Python cell modules importable.
31
+ _THIS = os.path.dirname(os.path.abspath(__file__))
32
+ _REPO = os.path.dirname(os.path.dirname(_THIS)) # repo root
33
+ _CELL_DIR = os.path.join(_REPO, "benchmark", "crosslang", "adapters", "python")
34
+ if _CELL_DIR not in sys.path:
35
+ sys.path.insert(0, _CELL_DIR)
36
+
37
+ from graphddb_runtime import GraphDDBRuntime # noqa: E402
38
+ from graphddb_cell import GraphddbCell # noqa: E402
39
+ from sdk_baseline import SdkBaseline, TABLE # noqa: E402
40
+ from micro_transport import MicroMockTransport, USER_ITEM, LIST_ROWS # noqa: E402
41
+
42
+ GENERATED = os.path.join(_CELL_DIR, "generated")
43
+ MANIFEST = os.path.join(GENERATED, "manifest.json")
44
+ OPERATIONS = os.path.join(GENERATED, "operations.json")
45
+
46
+ INPUTS = {
47
+ "readUserId": "u0",
48
+ "readEmail": "u0@example.com",
49
+ "readGroupId": "g0",
50
+ "putGroupId": "g99",
51
+ "updateUserId": "u1",
52
+ "joinGroupId": "g99",
53
+ }
54
+
55
+
56
+ def _client() -> boto3.client:
57
+ return boto3.client(
58
+ "dynamodb",
59
+ endpoint_url="http://localhost:8000",
60
+ region_name="local",
61
+ aws_access_key_id="fake",
62
+ aws_secret_access_key="fake",
63
+ config=Config(retries={"max_attempts": 1}),
64
+ )
65
+
66
+
67
+ def _profile_item(uid: str) -> dict:
68
+ return {
69
+ "PK": {"S": f"USER#{uid}"},
70
+ "SK": {"S": "PROFILE"},
71
+ "userId": {"S": uid},
72
+ "name": {"S": f"User {uid}"},
73
+ "email": {"S": f"{uid}@example.com"},
74
+ "status": {"S": "active"},
75
+ }
76
+
77
+
78
+ def _membership_rows(n: int) -> list:
79
+ return [
80
+ {
81
+ "PK": {"S": f"GROUP#g{i}"},
82
+ "SK": {"S": "USER#u0"},
83
+ "GSI1PK": {"S": "USER#u0"},
84
+ "GSI1SK": {"S": f"GROUP#g{i}"},
85
+ "userId": {"S": "u0"},
86
+ "groupId": {"S": f"g{i}"},
87
+ "role": {"S": "member"},
88
+ }
89
+ for i in range(n)
90
+ ]
91
+
92
+
93
+ # ── 1. SDK baseline correctness + cost (requests/op · items/op) ─────────────────
94
+
95
+
96
+ def test_sdk_pk_query_correct_and_projected():
97
+ c = _client()
98
+ stub = Stubber(c)
99
+ stub.add_response(
100
+ "get_item",
101
+ {"Item": _profile_item("u0")},
102
+ {
103
+ "TableName": TABLE,
104
+ "Key": {"PK": {"S": "USER#u0"}, "SK": {"S": "PROFILE"}},
105
+ "ProjectionExpression": "userId, #n, email",
106
+ "ExpressionAttributeNames": {"#n": "name"},
107
+ },
108
+ )
109
+ with stub:
110
+ cell = SdkBaseline(c)
111
+ result, requests, items = cell.pk_query("u0")
112
+ assert result["userId"] == "u0"
113
+ assert requests == 1 and items == 1
114
+
115
+
116
+ def test_sdk_list_query_limit_and_cost():
117
+ c = _client()
118
+ stub = Stubber(c)
119
+ stub.add_response(
120
+ "query",
121
+ {"Items": _membership_rows(20), "Count": 20, "ScannedCount": 20},
122
+ {
123
+ "TableName": TABLE,
124
+ "KeyConditionExpression": "PK = :pk AND begins_with(SK, :p)",
125
+ "ExpressionAttributeValues": {":pk": {"S": "GROUP#g0"}, ":p": {"S": "USER#"}},
126
+ "ProjectionExpression": "userId, #r",
127
+ "ExpressionAttributeNames": {"#r": "role"},
128
+ "Limit": 20,
129
+ },
130
+ )
131
+ with stub:
132
+ cell = SdkBaseline(c)
133
+ rows, requests, items = cell.list_query("g0")
134
+ assert len(rows) == 20 and requests == 1 and items == 20
135
+
136
+
137
+ def test_sdk_nested_batch_get_avoids_n_plus_1():
138
+ """user Get + memberships GSI Query + ONE BatchGetItem (never GetItem×N)."""
139
+ c = _client()
140
+ stub = Stubber(c)
141
+ stub.add_response("get_item", {"Item": {"userId": {"S": "u0"}}}, None)
142
+ stub.add_response(
143
+ "query",
144
+ {"Items": _membership_rows(3), "Count": 3, "ScannedCount": 3},
145
+ None,
146
+ )
147
+ stub.add_response(
148
+ "batch_get_item",
149
+ {
150
+ "Responses": {
151
+ TABLE: [
152
+ {"PK": {"S": f"GROUP#g{i}"}, "SK": {"S": "META"}, "groupId": {"S": f"g{i}"}, "name": {"S": f"Group {i}"}}
153
+ for i in range(3)
154
+ ]
155
+ }
156
+ },
157
+ None,
158
+ )
159
+ with stub:
160
+ cell = SdkBaseline(c)
161
+ result, requests, items = cell.nested_batch_get("u0")
162
+ # 3 physical requests (get + query + one batch), not 1 + N GetItems.
163
+ assert requests == 3
164
+ assert len(result["groups"]) == 3
165
+
166
+
167
+ def test_sdk_relation_multi_write_is_atomic_transaction():
168
+ c = _client()
169
+ stub = Stubber(c)
170
+
171
+ captured = {}
172
+
173
+ def _capture(params):
174
+ captured["items"] = params["TransactItems"]
175
+ return True
176
+
177
+ stub.add_response("transact_write_items", {}, None)
178
+ with stub:
179
+ cell = SdkBaseline(c, write_salt="python-sdk")
180
+ _r, requests, items = cell.relation_multi_write("g99", "member")
181
+ # ONE atomic TransactWriteItems (req/op 1), writes read 0 items.
182
+ assert requests == 1 and items == 0
183
+
184
+
185
+ # ── 2. graphddb cell correctness + same cost as the baseline (fairness) ─────────
186
+
187
+
188
+ def _runtime(client) -> GraphDDBRuntime:
189
+ return GraphDDBRuntime(
190
+ dynamodb_client=client, manifest_path=MANIFEST, operations_path=OPERATIONS
191
+ )
192
+
193
+
194
+ def test_graphddb_pk_query_hydrates_correctly():
195
+ c = _client()
196
+ stub = Stubber(c)
197
+ # #237: the graphddb runtime now pushes the `select` down as a server-side
198
+ # ProjectionExpression on the point GetItem (like the SDK baseline and the TS
199
+ # runtime), instead of reading the full item and projecting client-side. The
200
+ # request MUST carry a ProjectionExpression covering the selected fields
201
+ # (email/name/userId) plus the base-table key attrs (PK/SK). The stub's
202
+ # expected-params below asserts this fires — a plain GetItem (no projection)
203
+ # would now fail the stub.
204
+ stub.add_response(
205
+ "get_item",
206
+ {"Item": _profile_item("u0")},
207
+ {
208
+ "TableName": TABLE,
209
+ "Key": {"PK": {"S": "USER#u0"}, "SK": {"S": "PROFILE"}},
210
+ "ProjectionExpression": "#p0, #p1, #p2, #p3, #p4",
211
+ "ExpressionAttributeNames": {
212
+ "#p0": "email",
213
+ "#p1": "name",
214
+ "#p2": "userId",
215
+ "#p3": "PK",
216
+ "#p4": "SK",
217
+ },
218
+ },
219
+ )
220
+ with stub:
221
+ cell = GraphddbCell(_runtime(c))
222
+ result = cell.pk_query("u0")
223
+ assert result is not None and result["userId"] == "u0"
224
+ # projection applied server-side: only userId/name/email, not status.
225
+ assert "status" not in result
226
+ assert GraphddbCell.items_read(result) == 1
227
+
228
+
229
+ def test_graphddb_list_query_applies_limit():
230
+ """The graphddb range-contract path issues a Query with Limit=20 (no over-read)."""
231
+ c = _client()
232
+ stub = Stubber(c)
233
+ stub.add_response(
234
+ "query",
235
+ {"Items": _membership_rows(20), "Count": 20, "ScannedCount": 20},
236
+ None, # request-shape flexibility: assert Limit via a captured check below
237
+ )
238
+ with stub:
239
+ cell = GraphddbCell(_runtime(c))
240
+ result = cell.list_query("g0")
241
+ items = result["items"] if isinstance(result, dict) else result
242
+ assert len(items) == 20
243
+ assert GraphddbCell.items_read(result) == 20
244
+
245
+
246
+ def test_graphddb_list_query_sends_limit_20():
247
+ """Assert the physical Query carries Limit=20 (fairness with the SDK baseline)."""
248
+ c = _client()
249
+ seen = {}
250
+ orig = c.query
251
+
252
+ def _spy(**kwargs):
253
+ seen.update(kwargs)
254
+ return orig(**kwargs)
255
+
256
+ c.query = _spy # type: ignore[assignment]
257
+ stub = Stubber(c)
258
+ stub.add_response(
259
+ "query", {"Items": _membership_rows(20), "Count": 20, "ScannedCount": 20}, None
260
+ )
261
+ with stub:
262
+ GraphddbCell(_runtime(c)).list_query("g0")
263
+ assert seen.get("Limit") == 20
264
+
265
+
266
+ def test_graphddb_query_sends_projection_expression():
267
+ """#237: the physical Query pushes the `select` down as a server-side
268
+ ProjectionExpression (not client-side projection). Assert the wire request
269
+ carries a ProjectionExpression whose aliased names cover the selected fields
270
+ (role/userId) plus the partition key attr (PK)."""
271
+ c = _client()
272
+ seen = {}
273
+ orig = c.query
274
+
275
+ def _spy(**kwargs):
276
+ seen.update(kwargs)
277
+ return orig(**kwargs)
278
+
279
+ c.query = _spy # type: ignore[assignment]
280
+ stub = Stubber(c)
281
+ stub.add_response(
282
+ "query", {"Items": _membership_rows(20), "Count": 20, "ScannedCount": 20}, None
283
+ )
284
+ with stub:
285
+ GraphddbCell(_runtime(c)).list_query("g0")
286
+ proj = seen.get("ProjectionExpression")
287
+ assert proj, "Query must send a ProjectionExpression (#237)"
288
+ names = seen.get("ExpressionAttributeNames", {})
289
+ # Every projection token is an alias resolving to a real attribute name.
290
+ projected_attrs = {names[tok.strip()] for tok in proj.split(",")}
291
+ # The selected fields (+ partition key) must be projected server-side.
292
+ assert {"role", "userId", "PK"} <= projected_attrs
293
+
294
+
295
+ def test_graphddb_getitem_sends_projection_expression():
296
+ """#237: the physical point GetItem pushes the `select` down as a server-side
297
+ ProjectionExpression covering the selected fields + base-table key attrs."""
298
+ c = _client()
299
+ seen = {}
300
+ orig = c.get_item
301
+
302
+ def _spy(**kwargs):
303
+ seen.update(kwargs)
304
+ return orig(**kwargs)
305
+
306
+ c.get_item = _spy # type: ignore[assignment]
307
+ stub = Stubber(c)
308
+ stub.add_response("get_item", {"Item": _profile_item("u0")}, None)
309
+ with stub:
310
+ GraphddbCell(_runtime(c)).pk_query("u0")
311
+ proj = seen.get("ProjectionExpression")
312
+ assert proj, "GetItem must send a ProjectionExpression (#237)"
313
+ names = seen.get("ExpressionAttributeNames", {})
314
+ projected_attrs = {names[tok.strip()] for tok in proj.split(",")}
315
+ assert {"email", "name", "userId", "PK", "SK"} <= projected_attrs
316
+
317
+
318
+ def test_graphddb_relation_single_matches_baseline_cost():
319
+ """user + memberships (2 requests, items = 1 + rows) — same logical work as SDK."""
320
+ c = _client()
321
+ stub = Stubber(c)
322
+ stub.add_response("get_item", {"Item": {"userId": {"S": "u0"}}}, None)
323
+ stub.add_response(
324
+ "query", {"Items": _membership_rows(5), "Count": 5, "ScannedCount": 5}, None
325
+ )
326
+ with stub:
327
+ cell = GraphddbCell(_runtime(c))
328
+ result = cell.relation_single("u0")
329
+ # parent user (1) + 5 membership rows.
330
+ assert GraphddbCell.items_read(result) == 6
331
+
332
+
333
+ # ── 3. the micro mock genuinely EXCLUDES I/O (no socket) ────────────────────────
334
+
335
+
336
+ def test_micro_mock_excludes_io_no_socket(monkeypatch):
337
+ """With the mock installed, a full get_item runs but NO socket is ever opened."""
338
+ c = boto3.client(
339
+ "dynamodb",
340
+ endpoint_url="http://localhost:1", # nothing listens here
341
+ region_name="local",
342
+ aws_access_key_id="x",
343
+ aws_secret_access_key="y",
344
+ config=Config(retries={"max_attempts": 1}, connect_timeout=1, read_timeout=1),
345
+ )
346
+ mock = MicroMockTransport()
347
+ mock.install(c)
348
+
349
+ # Trip a hard failure if ANY real socket connect is attempted.
350
+ real_connect = socket.socket.connect
351
+
352
+ def _boom(self, *args, **kwargs): # noqa: ANN001
353
+ raise AssertionError("micro-bench opened a real socket — I/O NOT excluded")
354
+
355
+ monkeypatch.setattr(socket.socket, "connect", _boom)
356
+
357
+ resp = c.get_item(
358
+ TableName=TABLE,
359
+ Key={"PK": {"S": "USER#u0"}, "SK": {"S": "PROFILE"}},
360
+ ProjectionExpression="userId, #n, email",
361
+ ExpressionAttributeNames={"#n": "name"},
362
+ )
363
+ # The FULL client stack parsed the fixture (proves the stack ran, not a bypass).
364
+ assert resp["Item"]["userId"]["S"] == "u0"
365
+ assert mock.served == 1
366
+ # Restore for tidiness (monkeypatch would anyway).
367
+ monkeypatch.setattr(socket.socket, "connect", real_connect)
368
+
369
+
370
+ def test_micro_mock_full_client_stack_runs_query_and_batch():
371
+ """The mock serves Query (20 rows) and BatchGetItem, parsed by the real stack."""
372
+ c = _client()
373
+ mock = MicroMockTransport()
374
+ mock.install(c)
375
+ q = c.query(
376
+ TableName=TABLE,
377
+ KeyConditionExpression="PK = :pk AND begins_with(SK, :p)",
378
+ ExpressionAttributeValues={":pk": {"S": "GROUP#g0"}, ":p": {"S": "USER#"}},
379
+ Limit=20,
380
+ )
381
+ assert q["Count"] == LIST_ROWS and len(q["Items"]) == LIST_ROWS
382
+ b = c.batch_get_item(
383
+ RequestItems={TABLE: {"Keys": [{"PK": {"S": "GROUP#g0"}, "SK": {"S": "META"}}]}}
384
+ )
385
+ assert len(b["Responses"][TABLE]) == LIST_ROWS
386
+ assert mock.served == 2
387
+
388
+
389
+ def test_micro_mock_graphddb_cell_runs_without_io():
390
+ """The graphddb cell's client-side path runs end-to-end over the mock transport."""
391
+ c = _client()
392
+ mock = MicroMockTransport()
393
+ mock.install(c)
394
+ cell = GraphddbCell(_runtime(c))
395
+ # pkQuery hydrates the fixed USER_ITEM through the runtime; relationSingle builds
396
+ # the plan + hydrates the parent + 20 membership rows — all with no network.
397
+ user = cell.pk_query("u0")
398
+ assert user is not None and user["userId"] == USER_ITEM["userId"]["S"]
399
+ relation = cell.relation_single("u0")
400
+ assert GraphddbCell.items_read(relation) == 1 + LIST_ROWS
401
+ assert mock.served >= 3 # get + (get ∥ query for the relation)
402
+
403
+
404
+ def test_micro_fixtures_match_ts_shapes():
405
+ """The Python micro fixtures mirror the TS MICRO_FIXTURES (attr/row counts)."""
406
+ assert LIST_ROWS == 20
407
+ # USER_ITEM carries the 6 attributes the TS fixture does.
408
+ assert set(USER_ITEM.keys()) == {"PK", "SK", "userId", "name", "email", "status"}