graphddb-runtime 0.1.0__tar.gz → 0.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.
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/PKG-INFO +1 -1
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/batch.py +2 -1
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/runtime.py +70 -15
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime.egg-info/PKG-INFO +1 -1
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/pyproject.toml +1 -1
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_command.py +33 -14
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_unit.py +29 -20
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/README.md +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/__init__.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/async_runtime.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/concurrency.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/cursor.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/errors.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/filters.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/hydration.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/limits.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/per_key_cursor.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/relations.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/templates.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime/transactions.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime.egg-info/SOURCES.txt +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime.egg-info/dependency_links.txt +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime.egg-info/requires.txt +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime.egg-info/top_level.txt +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/setup.cfg +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_concurrency.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_contract_runtime.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_compose.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_contract.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_edge_derive.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_edge_write.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_events.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_referential.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_relations.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_integration_unique.py +0 -0
- {graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/tests/test_relations.py +0 -0
|
@@ -152,7 +152,8 @@ class BatchWriteExecutor:
|
|
|
152
152
|
|
|
153
153
|
``BatchWriteItem`` carries **no conditions** (DynamoDB has no per-request
|
|
154
154
|
``ConditionExpression`` for it) and is **not atomic** — both are properties of
|
|
155
|
-
the command-contract ``'
|
|
155
|
+
the command-contract ``mode: 'parallel'`` coalesced fan-out (issue #64/#101).
|
|
156
|
+
The sleep is injected
|
|
156
157
|
so unit tests can observe the backoff schedule without real delays.
|
|
157
158
|
"""
|
|
158
159
|
|
|
@@ -789,11 +789,11 @@ class GraphDDBRuntime:
|
|
|
789
789
|
the method's declared mode:
|
|
790
790
|
|
|
791
791
|
- ``single`` key → the one referenced write op in ``commands``;
|
|
792
|
-
- ``keys[]`` + ``
|
|
792
|
+
- ``keys[]`` + ``transaction`` → one ``TransactWriteItems`` (the synthesized
|
|
793
793
|
per-key ``forEach`` transaction; **atomic**, ≤25, condition-capable). An
|
|
794
794
|
array of **>25 keys is rejected** — an atomic transaction cannot be split.
|
|
795
|
-
- ``keys[]`` + ``
|
|
796
|
-
|
|
795
|
+
- ``keys[]`` + ``parallel`` → a non-atomic per-key fan-out with partial
|
|
796
|
+
success (unconditioned put/delete coalesce into a ``BatchWriteItem``).
|
|
797
797
|
|
|
798
798
|
``params`` are the mutation values shared across every key (the body's
|
|
799
799
|
``params`` argument). For each key the runtime merges ``{**key, **params}``
|
|
@@ -810,9 +810,15 @@ class GraphDDBRuntime:
|
|
|
810
810
|
shared = dict(params or {})
|
|
811
811
|
|
|
812
812
|
if isinstance(key_or_keys, list):
|
|
813
|
-
self._execute_command_batch(
|
|
813
|
+
outcome = self._execute_command_batch(
|
|
814
814
|
contract_name, method_name, method, key_or_keys, shared
|
|
815
815
|
)
|
|
816
|
+
# #101 `mode: 'parallel'` returns a partial-success result list; the
|
|
817
|
+
# atomic / batchWrite forms return None (fire-and-forget). Surface the
|
|
818
|
+
# parallel outcome to the caller wrapped as `{"results": [...]}`, matching
|
|
819
|
+
# the TS runtime's `CommandParallelReturn`.
|
|
820
|
+
if outcome is not None:
|
|
821
|
+
return {"results": outcome}
|
|
816
822
|
return None
|
|
817
823
|
|
|
818
824
|
# Single key → the referenced write surface, driven by key + params.
|
|
@@ -894,15 +900,19 @@ class GraphDDBRuntime:
|
|
|
894
900
|
method: Mapping[str, Any],
|
|
895
901
|
keys: List[Mapping[str, Any]],
|
|
896
902
|
shared: Mapping[str, Any],
|
|
897
|
-
) ->
|
|
898
|
-
"""Apply a command method's array form per its declared
|
|
903
|
+
) -> Optional[List[Dict[str, Any]]]:
|
|
904
|
+
"""Apply a command method's array form per its declared `mode` target.
|
|
905
|
+
|
|
906
|
+
Returns ``None`` for the atomic ``transaction`` form (fire-and-forget), or
|
|
907
|
+
the per-op partial-success result list for the #101 ``parallel`` form."""
|
|
899
908
|
batch = method.get("batch")
|
|
900
909
|
label = f"{contract_name}.{method_name}"
|
|
901
910
|
if batch is None:
|
|
902
911
|
raise ContractArityError(
|
|
903
912
|
f"command method '{label}' was called with an array of keys, but it "
|
|
904
|
-
f"declares no
|
|
905
|
-
f"
|
|
913
|
+
f"declares no key-array bulk form. Author the method with "
|
|
914
|
+
f"`mode: 'transaction'` / `mode: 'parallel'`, or call it with a "
|
|
915
|
+
f"single key."
|
|
906
916
|
)
|
|
907
917
|
mode = batch.get("mode")
|
|
908
918
|
if mode == "transaction":
|
|
@@ -912,13 +922,57 @@ class GraphDDBRuntime:
|
|
|
912
922
|
batch["transaction"], {**dict(shared), "keys": [dict(k) for k in keys]}
|
|
913
923
|
)
|
|
914
924
|
return
|
|
915
|
-
if mode == "
|
|
916
|
-
|
|
917
|
-
|
|
925
|
+
if mode == "parallel":
|
|
926
|
+
# #101 — non-atomic per-key fan-out with partial success. This branch
|
|
927
|
+
# returns a per-op result list; the caller (execute_command_method)
|
|
928
|
+
# surfaces it.
|
|
929
|
+
return self._execute_command_parallel(
|
|
930
|
+
label, batch["operation"], keys, shared
|
|
931
|
+
)
|
|
918
932
|
raise GraphDDBError( # pragma: no cover - serializer only emits the two above
|
|
919
933
|
f"{label}: unknown batch resolution mode '{mode}'"
|
|
920
934
|
)
|
|
921
935
|
|
|
936
|
+
def _execute_command_parallel(
|
|
937
|
+
self,
|
|
938
|
+
label: str,
|
|
939
|
+
command_id: str,
|
|
940
|
+
keys: List[Mapping[str, Any]],
|
|
941
|
+
shared: Mapping[str, Any],
|
|
942
|
+
) -> List[Dict[str, Any]]:
|
|
943
|
+
"""#101 ``mode: 'parallel'`` — non-atomic per-key fan-out, partial success.
|
|
944
|
+
|
|
945
|
+
Mirrors the TS ``executeParallelWrites``: when the per-key op carries no
|
|
946
|
+
condition and is put/delete, coalesce into a ``BatchWriteItem`` (chunk ≤25,
|
|
947
|
+
``UnprocessedItems`` retry) and report every key ``{"ok": True}``; otherwise
|
|
948
|
+
issue each conditional write individually, collecting ``{"ok": bool,
|
|
949
|
+
"error"?}`` per key. A per-op failure NEVER aborts the others. The result
|
|
950
|
+
list is aligned to the input key order.
|
|
951
|
+
"""
|
|
952
|
+
if not keys:
|
|
953
|
+
return []
|
|
954
|
+
spec = self._commands.get(command_id)
|
|
955
|
+
if spec is None:
|
|
956
|
+
raise ContractNotFoundError(
|
|
957
|
+
f"{label}: referenced write op '{command_id}' is not present in "
|
|
958
|
+
f"`commands`."
|
|
959
|
+
)
|
|
960
|
+
op_type = spec["type"]
|
|
961
|
+
has_condition = spec.get("condition") is not None
|
|
962
|
+
coalescible = not has_condition and op_type in ("PutItem", "DeleteItem")
|
|
963
|
+
if coalescible:
|
|
964
|
+
self._execute_batch_write(label, command_id, keys, shared)
|
|
965
|
+
return [{"ok": True} for _ in keys]
|
|
966
|
+
results: List[Dict[str, Any]] = []
|
|
967
|
+
for key in keys:
|
|
968
|
+
params = {**dict(key), **dict(shared)}
|
|
969
|
+
try:
|
|
970
|
+
self.execute_command(command_id, params)
|
|
971
|
+
results.append({"ok": True})
|
|
972
|
+
except Exception as exc: # noqa: BLE001 - per-op partial success
|
|
973
|
+
results.append({"ok": False, "error": str(exc)})
|
|
974
|
+
return results
|
|
975
|
+
|
|
922
976
|
def _execute_batch_write(
|
|
923
977
|
self,
|
|
924
978
|
label: str,
|
|
@@ -930,8 +984,9 @@ class GraphDDBRuntime:
|
|
|
930
984
|
|
|
931
985
|
Reuses :class:`BatchWriteExecutor` (chunk ≤25, ``UnprocessedItems`` retry).
|
|
932
986
|
DynamoDB's ``BatchWriteItem`` supports only ``PutRequest`` / ``DeleteRequest``
|
|
933
|
-
— a command whose single-key op is an ``UpdateItem`` cannot
|
|
934
|
-
array form
|
|
987
|
+
— a command whose single-key op is an ``UpdateItem`` cannot coalesce its
|
|
988
|
+
``mode: 'parallel'`` array form into a ``BatchWriteItem`` (it falls back to
|
|
989
|
+
per-key ``UpdateItem`` calls, or must declare ``mode: 'transaction'``).
|
|
935
990
|
"""
|
|
936
991
|
spec = self._commands.get(command_id)
|
|
937
992
|
if spec is None:
|
|
@@ -942,8 +997,8 @@ class GraphDDBRuntime:
|
|
|
942
997
|
op_type = spec["type"]
|
|
943
998
|
if op_type == "UpdateItem":
|
|
944
999
|
raise GraphDDBError(
|
|
945
|
-
f"{label}:
|
|
946
|
-
f"BatchWriteItem has no Update request). Declare
|
|
1000
|
+
f"{label}: BatchWriteItem only supports put / delete (DynamoDB's "
|
|
1001
|
+
f"BatchWriteItem has no Update request). Declare `mode: 'transaction'` "
|
|
947
1002
|
f"for a batched update."
|
|
948
1003
|
)
|
|
949
1004
|
requests: List[Dict[str, Any]] = []
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "graphddb-runtime"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
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"
|
|
@@ -6,11 +6,11 @@ fixtures (``UserCommands`` / ``MembershipCommands`` in operations.json) and
|
|
|
6
6
|
asserts the **actual persisted state** after each write:
|
|
7
7
|
|
|
8
8
|
- single key → one write op (update / put / delete);
|
|
9
|
-
- key array + '
|
|
10
|
-
condition: applies on success, rolls the WHOLE batch back on failure);
|
|
11
|
-
- key array + '
|
|
12
|
-
chunked > 25 with retry;
|
|
13
|
-
- a >25-key '
|
|
9
|
+
- key array + `mode: 'transaction'` → one atomic TransactWriteItems (with a
|
|
10
|
+
per-item condition: applies on success, rolls the WHOLE batch back on failure);
|
|
11
|
+
- key array + `mode: 'parallel'` → a non-atomic per-key fan-out (put / delete
|
|
12
|
+
coalesce into BatchWriteItem), chunked > 25 with retry;
|
|
13
|
+
- a >25-key `mode: 'transaction'` array is rejected (an atomic batch cannot be split).
|
|
14
14
|
|
|
15
15
|
This mirrors the TS suite (__tests__/integration/command-runtime.test.ts) over
|
|
16
16
|
the **same SSoT**, proving the two runtimes produce identical effects.
|
|
@@ -147,7 +147,10 @@ def test_single_update_applies(rt, client):
|
|
|
147
147
|
|
|
148
148
|
def test_single_put_then_delete(rt, client):
|
|
149
149
|
rt.execute_command_method(
|
|
150
|
-
"MembershipCommands",
|
|
150
|
+
"MembershipCommands",
|
|
151
|
+
"add",
|
|
152
|
+
{"groupId": "eng", "userId": "solo"},
|
|
153
|
+
{"role": "lead", "joinedAt": "2021-06-01T00:00:00.000Z"},
|
|
151
154
|
)
|
|
152
155
|
row = _membership(client, "eng", "solo")
|
|
153
156
|
assert row is not None and row["role"]["S"] == "lead"
|
|
@@ -172,9 +175,21 @@ def test_transact_array_applies_atomically(rt, client):
|
|
|
172
175
|
|
|
173
176
|
|
|
174
177
|
def test_transact_condition_rolls_back_whole_batch_on_failure(rt, client):
|
|
175
|
-
# Make e 'pending' so its `status = active` condition fails; the atomic
|
|
176
|
-
# must then roll d back too.
|
|
177
|
-
|
|
178
|
+
# Make e 'pending' so its literal `status = active` condition fails; the atomic
|
|
179
|
+
# batch must then roll d back too. (`disable`'s input is `literal('disabled')`,
|
|
180
|
+
# so the 'pending' precondition is written with a raw put rather than the command.)
|
|
181
|
+
client.put_item(
|
|
182
|
+
TableName=TABLE,
|
|
183
|
+
Item={
|
|
184
|
+
"PK": {"S": "USER#e"},
|
|
185
|
+
"SK": {"S": "PROFILE"},
|
|
186
|
+
"userId": {"S": "e"},
|
|
187
|
+
"name": {"S": "User e"},
|
|
188
|
+
"email": {"S": "e@x.com"},
|
|
189
|
+
"status": {"S": "pending"},
|
|
190
|
+
"createdAt": {"S": "2021-01-01T00:00:00.000Z"},
|
|
191
|
+
},
|
|
192
|
+
)
|
|
178
193
|
assert _user_status(client, "e") == "pending"
|
|
179
194
|
|
|
180
195
|
with pytest.raises(OperationExecutionError):
|
|
@@ -182,7 +197,7 @@ def test_transact_condition_rolls_back_whole_batch_on_failure(rt, client):
|
|
|
182
197
|
"UserCommands",
|
|
183
198
|
"disableIfActive",
|
|
184
199
|
[{"userId": "d"}, {"userId": "e"}],
|
|
185
|
-
{"status": "disabled"
|
|
200
|
+
{"status": "disabled"},
|
|
186
201
|
)
|
|
187
202
|
# d unchanged (rolled back); e unchanged.
|
|
188
203
|
assert _user_status(client, "d") == "active"
|
|
@@ -194,7 +209,7 @@ def test_transact_condition_applies_when_all_hold(rt, client):
|
|
|
194
209
|
"UserCommands",
|
|
195
210
|
"disableIfActive",
|
|
196
211
|
[{"userId": "f"}],
|
|
197
|
-
{"status": "disabled"
|
|
212
|
+
{"status": "disabled"},
|
|
198
213
|
)
|
|
199
214
|
assert _user_status(client, "f") == "disabled"
|
|
200
215
|
|
|
@@ -205,12 +220,14 @@ def test_transact_rejects_over_25_keys(rt, client):
|
|
|
205
220
|
rt.execute_command_method("UserCommands", "disable", keys, {"status": "disabled"})
|
|
206
221
|
|
|
207
222
|
|
|
208
|
-
# ── array +
|
|
223
|
+
# ── array + mode:'parallel' → non-atomic BatchWriteItem (chunked > 25) ───────
|
|
209
224
|
|
|
210
225
|
|
|
211
226
|
def test_batch_write_puts_and_chunks_over_25(rt, client):
|
|
212
227
|
keys = [{"groupId": "big", "userId": f"m{i}"} for i in range(30)]
|
|
213
|
-
rt.execute_command_method(
|
|
228
|
+
rt.execute_command_method(
|
|
229
|
+
"MembershipCommands", "add", keys, {"role": "member", "joinedAt": "2021-06-01T00:00:00.000Z"}
|
|
230
|
+
)
|
|
214
231
|
assert _membership(client, "big", "m0")["role"]["S"] == "member"
|
|
215
232
|
assert _membership(client, "big", "m24")["role"]["S"] == "member"
|
|
216
233
|
assert _membership(client, "big", "m29")["role"]["S"] == "member"
|
|
@@ -218,7 +235,9 @@ def test_batch_write_puts_and_chunks_over_25(rt, client):
|
|
|
218
235
|
|
|
219
236
|
def test_batch_write_deletes(rt, client):
|
|
220
237
|
keys = [{"groupId": "big", "userId": f"m{i}"} for i in range(30)]
|
|
221
|
-
rt.execute_command_method(
|
|
238
|
+
rt.execute_command_method(
|
|
239
|
+
"MembershipCommands", "add", keys, {"role": "member", "joinedAt": "2021-06-01T00:00:00.000Z"}
|
|
240
|
+
)
|
|
222
241
|
rt.execute_command_method("MembershipCommands", "remove", keys)
|
|
223
242
|
assert _membership(client, "big", "m0") is None
|
|
224
243
|
assert _membership(client, "big", "m29") is None
|
|
@@ -901,7 +901,7 @@ def test_command_method_single_update_runs_one_update_item():
|
|
|
901
901
|
|
|
902
902
|
|
|
903
903
|
def test_command_method_transact_array_runs_one_transaction():
|
|
904
|
-
"""A key array + '
|
|
904
|
+
"""A key array + `mode: 'transaction'` → ONE TransactWriteItems (one item per key)."""
|
|
905
905
|
client = FakeClient()
|
|
906
906
|
rt = make_runtime(client)
|
|
907
907
|
rt.execute_command_method(
|
|
@@ -920,26 +920,28 @@ def test_command_method_transact_array_runs_one_transaction():
|
|
|
920
920
|
|
|
921
921
|
|
|
922
922
|
def test_command_method_transact_carries_per_item_condition():
|
|
923
|
-
"""A conditional '
|
|
923
|
+
"""A conditional `mode: 'transaction'` batch attaches the equality condition per item."""
|
|
924
924
|
client = FakeClient()
|
|
925
925
|
rt = make_runtime(client)
|
|
926
926
|
rt.execute_command_method(
|
|
927
927
|
"UserCommands",
|
|
928
928
|
"disableIfActive",
|
|
929
929
|
[{"userId": "a"}, {"userId": "b"}],
|
|
930
|
-
{"status": "disabled"
|
|
930
|
+
{"status": "disabled"},
|
|
931
931
|
)
|
|
932
932
|
method, req = client.calls[0]
|
|
933
933
|
assert method == "transact_write_items"
|
|
934
934
|
update = req["TransactItems"][0]["Update"]
|
|
935
935
|
assert "ConditionExpression" in update
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
]
|
|
936
|
+
# #101 descriptor form: the gate is a concrete-literal equality (status == 'active');
|
|
937
|
+
# the written value is the 'disabled' status param. Both appear per item.
|
|
938
|
+
values = [_DESER.deserialize(v) for v in update["ExpressionAttributeValues"].values()]
|
|
939
|
+
assert "active" in values
|
|
940
|
+
assert "disabled" in values
|
|
939
941
|
|
|
940
942
|
|
|
941
943
|
def test_command_method_transact_rejects_over_25_keys():
|
|
942
|
-
"""A >25-key '
|
|
944
|
+
"""A >25-key `mode: 'transaction'` array is rejected — an atomic tx cannot be split."""
|
|
943
945
|
client = FakeClient()
|
|
944
946
|
rt = make_runtime(client)
|
|
945
947
|
keys = [{"userId": f"u{i}"} for i in range(26)]
|
|
@@ -949,32 +951,38 @@ def test_command_method_transact_rejects_over_25_keys():
|
|
|
949
951
|
assert all(m != "transact_write_items" for m, _ in client.calls)
|
|
950
952
|
|
|
951
953
|
|
|
952
|
-
def
|
|
953
|
-
"""
|
|
954
|
+
def test_command_method_parallel_guarded_create_runs_individual_conditional_puts():
|
|
955
|
+
"""#101 `mode: 'parallel'` + a guarded create (attribute_not_exists) → INDIVIDUAL
|
|
956
|
+
conditional PutItem writes (not a condition-less BatchWriteItem), with per-key
|
|
957
|
+
partial-success results aligned to key order."""
|
|
954
958
|
client = FakeClient()
|
|
955
959
|
rt = make_runtime(client)
|
|
956
|
-
rt.execute_command_method(
|
|
960
|
+
out = rt.execute_command_method(
|
|
957
961
|
"MembershipCommands",
|
|
958
962
|
"add",
|
|
959
963
|
[{"groupId": "eng", "userId": "a"}, {"groupId": "eng", "userId": "b"}],
|
|
960
|
-
{"role": "member"},
|
|
964
|
+
{"role": "member", "joinedAt": "1970-01-01T00:00:00Z"},
|
|
961
965
|
)
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
966
|
+
# A guarded create carries a condition, so the parallel path issues one PutItem
|
|
967
|
+
# per key (concurrently) — never a coalesced BatchWriteItem.
|
|
968
|
+
methods = [m for m, _ in client.calls]
|
|
969
|
+
assert methods == ["put_item", "put_item"]
|
|
970
|
+
first = client.calls[0][1]
|
|
971
|
+
assert "ConditionExpression" in first
|
|
972
|
+
item0 = plain(first["Item"])
|
|
968
973
|
assert item0["PK"] == "GROUP#eng"
|
|
969
974
|
assert item0["SK"] == "USER#a"
|
|
970
975
|
assert item0["role"] == "member"
|
|
976
|
+
# Partial-success result: both keys ok, aligned to input order.
|
|
977
|
+
assert out == {"results": [{"ok": True}, {"ok": True}]}
|
|
971
978
|
|
|
972
979
|
|
|
973
|
-
def
|
|
974
|
-
"""
|
|
980
|
+
def test_command_method_parallel_unconditioned_delete_coalesces_batch_write():
|
|
981
|
+
"""#101 `mode: 'parallel'` + an UNconditioned delete coalesces into a
|
|
982
|
+
BatchWriteItem (DeleteRequests), reporting every key ok."""
|
|
975
983
|
client = FakeClient()
|
|
976
984
|
rt = make_runtime(client)
|
|
977
|
-
rt.execute_command_method(
|
|
985
|
+
out = rt.execute_command_method(
|
|
978
986
|
"MembershipCommands",
|
|
979
987
|
"remove",
|
|
980
988
|
[{"groupId": "eng", "userId": "a"}, {"groupId": "eng", "userId": "b"}],
|
|
@@ -986,6 +994,7 @@ def test_command_method_batch_write_delete_runs_delete_requests():
|
|
|
986
994
|
"PK": "GROUP#eng",
|
|
987
995
|
"SK": "USER#a",
|
|
988
996
|
}
|
|
997
|
+
assert out == {"results": [{"ok": True}, {"ok": True}]}
|
|
989
998
|
|
|
990
999
|
|
|
991
1000
|
def test_command_method_unknown_contract_raises():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphddb_runtime-0.1.0 → graphddb_runtime-0.2.0}/graphddb_runtime.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|