idemkit 0.1.0__tar.gz → 0.1.1__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.
- {idemkit-0.1.0 → idemkit-0.1.1}/CHANGELOG.md +12 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/PKG-INFO +6 -7
- {idemkit-0.1.0 → idemkit-0.1.1}/README.md +5 -6
- idemkit-0.1.1/idemkit/_version.py +1 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/ai.py +28 -3
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/dynamodb.py +27 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_dynamodb_backend.py +11 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/method/test_ai_tool.py +18 -0
- idemkit-0.1.0/idemkit/_version.py +0 -1
- {idemkit-0.1.0 → idemkit-0.1.1}/.gitignore +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/CONTRIBUTING.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/Makefile +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/SECURITY.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/benchmarks/README.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/benchmarks/bench.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/docs/configuration.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/docs/correctness.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/docs/operations.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/docs/the-four-assumptions.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/README.md +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/all_options.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/django_wsgi.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/drf_view.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/fastapi_middleware.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/fastapi_route.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/flask_wsgi.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/getting_started.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/response_hook.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/route_decorator.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/webhook.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/agent_loop.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/all_options.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/cron_run_once.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/error_replay.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/exceptions.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/getting_started.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/local_cache.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/mcp.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/payload_validation.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/reconciliation.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/record_expiration.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/result_codecs.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/sync_function.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/all_options.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/cache_result.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/dead_letter.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/generic_broker.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/getting_started.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/kafka.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/payload_validation.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/sqs.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/shared/backends.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/shared/custom_backend.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/examples/shared/observability.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/asgi.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/queue.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/route.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/wsgi.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/base.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/memory.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/mongo.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/postgres.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/redis.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/cli.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/conformance/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/conformance/backend.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/conformance/report.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/drf.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/fastapi.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/kafka.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/logging.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/mcp.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/prometheus.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/pubsub.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/rabbitmq.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/reconciliation.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/sqs.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/codecs.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/config.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/engine.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/events.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/exception_cache.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/exceptions.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/fingerprint.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/policy.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/runner.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/state.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/sync_bridge.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/problem_details.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/py.typed +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/pyproject.toml +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/_backends.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_backend_contract.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_concurrent_claim.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_max_size.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_mongo_backend.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_postgres_backend.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_postgres_verify_schema.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_redis_backend.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/cli/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/cli/test_cli.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/conformance/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/conformance/test_conformance_suite.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/conftest.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_contrib.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_drf.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_fastapi.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_observability.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_codecs.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_core_runner.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_fingerprint.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_manual_clock.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_policy.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_sync_bridge.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/test_clock_skew.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/test_fault_injection.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/test_property_stateful.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/conftest.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/docker-compose.yml +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_http_endpoint.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_kafka.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_rabbitmq.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_sqs.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/_examples.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_http_examples.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_method_examples.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_queue_examples.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_shared_examples.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/fakes.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_asgi_middleware.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_asgi_with_real_backends.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_body_size.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_config.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_engine.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_header_filtering.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_observability.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_redactor.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_route_decorator.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_storage_errors.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_wsgi_middleware.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/method/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/method/test_nested_keys.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/queue/__init__.py +0 -0
- {idemkit-0.1.0 → idemkit-0.1.1}/tests/queue/test_queue_consumer.py +0 -0
|
@@ -9,6 +9,18 @@ minor version may carry a breaking change, and any such change is called out her
|
|
|
9
9
|
|
|
10
10
|
Nothing yet.
|
|
11
11
|
|
|
12
|
+
## [0.1.1] - 2026-07-26
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `DynamoBackend.from_url(endpoint_url)`, for parity with the `from_url`
|
|
16
|
+
constructors on the other backends.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- The "volatile-looking field" warning no longer fires on ordinary business keys
|
|
20
|
+
such as `order_id` or `customer_id`. It now matches only known per-call /
|
|
21
|
+
per-request id names (`request_id`, `trace_id`, `tool_call_id`, and similar), so
|
|
22
|
+
the getting-started example no longer warns about its own key.
|
|
23
|
+
|
|
12
24
|
## [0.1.0] - 2026-07-26
|
|
13
25
|
|
|
14
26
|
First public release. Pre-1.0: the API may still shift, and a minor version may
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: idemkit
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Make any operation safe to retry: crash-safe idempotency for HTTP requests, queue messages, and function calls, on Redis, Postgres, MongoDB, or DynamoDB.
|
|
5
5
|
Project-URL: Homepage, https://github.com/idemkit/idemkit
|
|
6
6
|
Project-URL: Documentation, https://github.com/idemkit/idemkit/blob/main/python/README.md
|
|
@@ -62,9 +62,11 @@ Description-Content-Type: text/markdown
|
|
|
62
62
|
|
|
63
63
|
# idemkit for Python
|
|
64
64
|
|
|
65
|
+
[](https://pypi.org/project/idemkit/)
|
|
65
66
|
[](https://github.com/idemkit/idemkit/actions/workflows/ci.yml)
|
|
66
67
|
[](../LICENSE)
|
|
67
68
|
[](https://www.python.org)
|
|
69
|
+
[](https://pypi.org/project/idemkit/)
|
|
68
70
|
|
|
69
71
|
Make any operation safe to retry. When a client retries, a broker redelivers, or an agent re-plans, idemkit runs your code **once per key** and replays the first result to the duplicates, even when they arrive at the same instant.
|
|
70
72
|
|
|
@@ -87,14 +89,11 @@ New to the vocabulary (*lease*, *fencing token*, *scope*, *fingerprint*)? Each l
|
|
|
87
89
|
|
|
88
90
|
## Install
|
|
89
91
|
|
|
90
|
-
Not on PyPI yet — install from source:
|
|
91
|
-
|
|
92
92
|
```bash
|
|
93
|
-
|
|
94
|
-
pip install -e ".[asgi,redis]" # pick the extras you need
|
|
93
|
+
pip install "idemkit[redis]" # or [postgres], [mongo], [dynamodb], [asgi]
|
|
95
94
|
```
|
|
96
95
|
|
|
97
|
-
|
|
96
|
+
Working on idemkit itself? Install from source instead: see [Contributing](#contributing).
|
|
98
97
|
|
|
99
98
|
The core has no third-party dependencies. Runs on Python 3.10 to 3.13, Redis 6+/Cluster, PostgreSQL 12+, MongoDB 4.2+, DynamoDB, any ASGI 3 or WSGI app.
|
|
100
99
|
|
|
@@ -117,7 +116,7 @@ The middleware wraps the whole app, but by default it only acts on `POST` and `P
|
|
|
117
116
|
Works in 30 seconds, no infrastructure (the `[asgi]` extra ships Starlette; this snippet also uses FastAPI):
|
|
118
117
|
|
|
119
118
|
```bash
|
|
120
|
-
pip install
|
|
119
|
+
pip install "idemkit[asgi]" fastapi uvicorn
|
|
121
120
|
```
|
|
122
121
|
|
|
123
122
|
```python
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# idemkit for Python
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/idemkit/)
|
|
3
4
|
[](https://github.com/idemkit/idemkit/actions/workflows/ci.yml)
|
|
4
5
|
[](../LICENSE)
|
|
5
6
|
[](https://www.python.org)
|
|
7
|
+
[](https://pypi.org/project/idemkit/)
|
|
6
8
|
|
|
7
9
|
Make any operation safe to retry. When a client retries, a broker redelivers, or an agent re-plans, idemkit runs your code **once per key** and replays the first result to the duplicates, even when they arrive at the same instant.
|
|
8
10
|
|
|
@@ -25,14 +27,11 @@ New to the vocabulary (*lease*, *fencing token*, *scope*, *fingerprint*)? Each l
|
|
|
25
27
|
|
|
26
28
|
## Install
|
|
27
29
|
|
|
28
|
-
Not on PyPI yet — install from source:
|
|
29
|
-
|
|
30
30
|
```bash
|
|
31
|
-
|
|
32
|
-
pip install -e ".[asgi,redis]" # pick the extras you need
|
|
31
|
+
pip install "idemkit[redis]" # or [postgres], [mongo], [dynamodb], [asgi]
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
Working on idemkit itself? Install from source instead: see [Contributing](#contributing).
|
|
36
35
|
|
|
37
36
|
The core has no third-party dependencies. Runs on Python 3.10 to 3.13, Redis 6+/Cluster, PostgreSQL 12+, MongoDB 4.2+, DynamoDB, any ASGI 3 or WSGI app.
|
|
38
37
|
|
|
@@ -55,7 +54,7 @@ The middleware wraps the whole app, but by default it only acts on `POST` and `P
|
|
|
55
54
|
Works in 30 seconds, no infrastructure (the `[asgi]` extra ships Starlette; this snippet also uses FastAPI):
|
|
56
55
|
|
|
57
56
|
```bash
|
|
58
|
-
pip install
|
|
57
|
+
pip install "idemkit[asgi]" fastapi uvicorn
|
|
59
58
|
```
|
|
60
59
|
|
|
61
60
|
```python
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -661,8 +661,29 @@ def _canonical_hash(selected: Mapping[str, Any]) -> str:
|
|
|
661
661
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
662
662
|
|
|
663
663
|
|
|
664
|
+
# Field NAMES that read as per-call / per-request identifiers: they change on every
|
|
665
|
+
# retry, so they must never be the idempotency key. A business key that merely ends
|
|
666
|
+
# in "_id" (order_id, customer_id, payment_id) is the RIGHT thing to key on and is
|
|
667
|
+
# deliberately NOT in this set.
|
|
664
668
|
_VOLATILE_NAMES = frozenset(
|
|
665
|
-
{
|
|
669
|
+
{
|
|
670
|
+
"request_id",
|
|
671
|
+
"requestid",
|
|
672
|
+
"trace_id",
|
|
673
|
+
"traceid",
|
|
674
|
+
"span_id",
|
|
675
|
+
"spanid",
|
|
676
|
+
"correlation_id",
|
|
677
|
+
"correlationid",
|
|
678
|
+
"tool_call_id",
|
|
679
|
+
"toolcallid",
|
|
680
|
+
"call_id",
|
|
681
|
+
"callid",
|
|
682
|
+
"nonce",
|
|
683
|
+
"timestamp",
|
|
684
|
+
"ts",
|
|
685
|
+
"idempotency_key",
|
|
686
|
+
}
|
|
666
687
|
)
|
|
667
688
|
_UUID_RE = re.compile(
|
|
668
689
|
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
@@ -709,10 +730,14 @@ def _value_looks_like_call_id(value: str) -> bool:
|
|
|
709
730
|
def _name_looks_volatile(name: str) -> bool:
|
|
710
731
|
"""Whether a field name alone (no value) reads as volatile.
|
|
711
732
|
|
|
712
|
-
Used at decoration time, where only the names in ``key_fields`` are known.
|
|
733
|
+
Used at decoration time, where only the names in ``key_fields`` are known. It
|
|
734
|
+
matches KNOWN per-call / per-request id names (request_id, trace_id,
|
|
735
|
+
tool_call_id, ...), not every field ending in ``_id``: a business key like
|
|
736
|
+
``order_id`` or ``customer_id`` is exactly what you should key on, so it must
|
|
737
|
+
not warn.
|
|
713
738
|
"""
|
|
714
739
|
lowered = name.lower()
|
|
715
|
-
if lowered in _VOLATILE_NAMES
|
|
740
|
+
if lowered in _VOLATILE_NAMES:
|
|
716
741
|
return True
|
|
717
742
|
return "timestamp" in lowered or "nonce" in lowered
|
|
718
743
|
|
|
@@ -87,6 +87,33 @@ class DynamoBackend:
|
|
|
87
87
|
|
|
88
88
|
self._init_lock = asyncio.Lock()
|
|
89
89
|
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_url(
|
|
92
|
+
cls,
|
|
93
|
+
url: str,
|
|
94
|
+
*,
|
|
95
|
+
table: str = DEFAULT_TABLE,
|
|
96
|
+
region_name: str = "us-east-1",
|
|
97
|
+
create_table: bool = True,
|
|
98
|
+
lease_grace_seconds: float = 60.0,
|
|
99
|
+
**client_kwargs: Any,
|
|
100
|
+
) -> DynamoBackend:
|
|
101
|
+
"""Construct from a DynamoDB endpoint URL, for parity with the other backends.
|
|
102
|
+
|
|
103
|
+
``url`` is the DynamoDB endpoint, e.g. ``http://localhost:8000`` for DynamoDB
|
|
104
|
+
Local or LocalStack. On real AWS you usually need no endpoint: build
|
|
105
|
+
``DynamoBackend(region_name=...)`` directly and let botocore resolve it. Extra
|
|
106
|
+
keyword arguments go to the aioboto3 resource (e.g. credentials).
|
|
107
|
+
"""
|
|
108
|
+
return cls(
|
|
109
|
+
table=table,
|
|
110
|
+
endpoint_url=url,
|
|
111
|
+
region_name=region_name,
|
|
112
|
+
create_table=create_table,
|
|
113
|
+
lease_grace_seconds=lease_grace_seconds,
|
|
114
|
+
**client_kwargs,
|
|
115
|
+
)
|
|
116
|
+
|
|
90
117
|
async def _ensure_table(self) -> Any:
|
|
91
118
|
if self._table is not None:
|
|
92
119
|
return self._table
|
|
@@ -29,6 +29,17 @@ def _dynamo(table: str, **kwargs):
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
def test_from_url_matches_direct_construction() -> None:
|
|
33
|
+
"""`from_url` exists for parity with the other backends and maps the URL to the
|
|
34
|
+
endpoint, without connecting (lazy)."""
|
|
35
|
+
from idemkit.backends.dynamodb import DynamoBackend
|
|
36
|
+
|
|
37
|
+
backend = DynamoBackend.from_url(DYNAMODB_ENDPOINT, table="idempotency_keys")
|
|
38
|
+
assert backend._endpoint_url == DYNAMODB_ENDPOINT
|
|
39
|
+
assert backend._table_name == "idempotency_keys"
|
|
40
|
+
assert backend._create_table is True
|
|
41
|
+
|
|
42
|
+
|
|
32
43
|
def _k(suffix: str) -> str:
|
|
33
44
|
return f"dynamo-{suffix}-{uuid.uuid4().hex}"
|
|
34
45
|
|
|
@@ -527,6 +527,24 @@ def test_volatile_field_named_in_key_fields_warns(backend) -> None:
|
|
|
527
527
|
return {"r": request_id}
|
|
528
528
|
|
|
529
529
|
|
|
530
|
+
def test_business_id_key_does_not_warn(backend) -> None:
|
|
531
|
+
"""A business key ending in _id (order_id, customer_id) is the RIGHT key to
|
|
532
|
+
dedupe on and must NOT trip the volatile-field warning (fixed false positive)."""
|
|
533
|
+
import warnings
|
|
534
|
+
|
|
535
|
+
with warnings.catch_warnings():
|
|
536
|
+
warnings.simplefilter("error") # any UserWarning here becomes a test failure
|
|
537
|
+
|
|
538
|
+
@idempotent(
|
|
539
|
+
backend=backend,
|
|
540
|
+
config=MethodConfig(scope=lambda a: "s", key_fields=["order_id", "customer_id"]),
|
|
541
|
+
)
|
|
542
|
+
async def charge(*, order_id, customer_id):
|
|
543
|
+
return {"order_id": order_id}
|
|
544
|
+
|
|
545
|
+
assert charge is not None
|
|
546
|
+
|
|
547
|
+
|
|
530
548
|
def test_key_fields_allowed_with_var_keyword(backend) -> None:
|
|
531
549
|
"""A tool taking **kwargs can name key fields not in its explicit signature —
|
|
532
550
|
we can't validate those, so don't reject them."""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.0"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|