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.
Files changed (157) hide show
  1. {idemkit-0.1.0 → idemkit-0.1.1}/CHANGELOG.md +12 -0
  2. {idemkit-0.1.0 → idemkit-0.1.1}/PKG-INFO +6 -7
  3. {idemkit-0.1.0 → idemkit-0.1.1}/README.md +5 -6
  4. idemkit-0.1.1/idemkit/_version.py +1 -0
  5. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/ai.py +28 -3
  6. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/dynamodb.py +27 -0
  7. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_dynamodb_backend.py +11 -0
  8. {idemkit-0.1.0 → idemkit-0.1.1}/tests/method/test_ai_tool.py +18 -0
  9. idemkit-0.1.0/idemkit/_version.py +0 -1
  10. {idemkit-0.1.0 → idemkit-0.1.1}/.gitignore +0 -0
  11. {idemkit-0.1.0 → idemkit-0.1.1}/CONTRIBUTING.md +0 -0
  12. {idemkit-0.1.0 → idemkit-0.1.1}/Makefile +0 -0
  13. {idemkit-0.1.0 → idemkit-0.1.1}/SECURITY.md +0 -0
  14. {idemkit-0.1.0 → idemkit-0.1.1}/benchmarks/README.md +0 -0
  15. {idemkit-0.1.0 → idemkit-0.1.1}/benchmarks/bench.py +0 -0
  16. {idemkit-0.1.0 → idemkit-0.1.1}/docs/configuration.md +0 -0
  17. {idemkit-0.1.0 → idemkit-0.1.1}/docs/correctness.md +0 -0
  18. {idemkit-0.1.0 → idemkit-0.1.1}/docs/operations.md +0 -0
  19. {idemkit-0.1.0 → idemkit-0.1.1}/docs/the-four-assumptions.md +0 -0
  20. {idemkit-0.1.0 → idemkit-0.1.1}/examples/README.md +0 -0
  21. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/all_options.py +0 -0
  22. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/django_wsgi.py +0 -0
  23. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/drf_view.py +0 -0
  24. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/fastapi_middleware.py +0 -0
  25. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/fastapi_route.py +0 -0
  26. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/flask_wsgi.py +0 -0
  27. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/getting_started.py +0 -0
  28. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/response_hook.py +0 -0
  29. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/route_decorator.py +0 -0
  30. {idemkit-0.1.0 → idemkit-0.1.1}/examples/http/webhook.py +0 -0
  31. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/agent_loop.py +0 -0
  32. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/all_options.py +0 -0
  33. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/cron_run_once.py +0 -0
  34. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/error_replay.py +0 -0
  35. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/exceptions.py +0 -0
  36. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/getting_started.py +0 -0
  37. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/local_cache.py +0 -0
  38. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/mcp.py +0 -0
  39. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/payload_validation.py +0 -0
  40. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/reconciliation.py +0 -0
  41. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/record_expiration.py +0 -0
  42. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/result_codecs.py +0 -0
  43. {idemkit-0.1.0 → idemkit-0.1.1}/examples/method/sync_function.py +0 -0
  44. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/all_options.py +0 -0
  45. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/cache_result.py +0 -0
  46. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/dead_letter.py +0 -0
  47. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/generic_broker.py +0 -0
  48. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/getting_started.py +0 -0
  49. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/kafka.py +0 -0
  50. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/payload_validation.py +0 -0
  51. {idemkit-0.1.0 → idemkit-0.1.1}/examples/queue/sqs.py +0 -0
  52. {idemkit-0.1.0 → idemkit-0.1.1}/examples/shared/backends.py +0 -0
  53. {idemkit-0.1.0 → idemkit-0.1.1}/examples/shared/custom_backend.py +0 -0
  54. {idemkit-0.1.0 → idemkit-0.1.1}/examples/shared/observability.py +0 -0
  55. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/__init__.py +0 -0
  56. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/__init__.py +0 -0
  57. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/asgi.py +0 -0
  58. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/queue.py +0 -0
  59. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/route.py +0 -0
  60. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/adapters/wsgi.py +0 -0
  61. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/__init__.py +0 -0
  62. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/base.py +0 -0
  63. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/memory.py +0 -0
  64. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/mongo.py +0 -0
  65. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/postgres.py +0 -0
  66. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/backends/redis.py +0 -0
  67. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/cli.py +0 -0
  68. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/conformance/__init__.py +0 -0
  69. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/conformance/backend.py +0 -0
  70. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/conformance/report.py +0 -0
  71. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/__init__.py +0 -0
  72. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/drf.py +0 -0
  73. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/fastapi.py +0 -0
  74. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/kafka.py +0 -0
  75. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/logging.py +0 -0
  76. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/mcp.py +0 -0
  77. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/prometheus.py +0 -0
  78. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/pubsub.py +0 -0
  79. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/rabbitmq.py +0 -0
  80. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/reconciliation.py +0 -0
  81. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/contrib/sqs.py +0 -0
  82. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/__init__.py +0 -0
  83. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/codecs.py +0 -0
  84. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/config.py +0 -0
  85. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/engine.py +0 -0
  86. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/events.py +0 -0
  87. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/exception_cache.py +0 -0
  88. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/exceptions.py +0 -0
  89. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/fingerprint.py +0 -0
  90. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/policy.py +0 -0
  91. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/runner.py +0 -0
  92. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/state.py +0 -0
  93. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/core/sync_bridge.py +0 -0
  94. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/problem_details.py +0 -0
  95. {idemkit-0.1.0 → idemkit-0.1.1}/idemkit/py.typed +0 -0
  96. {idemkit-0.1.0 → idemkit-0.1.1}/pyproject.toml +0 -0
  97. {idemkit-0.1.0 → idemkit-0.1.1}/tests/__init__.py +0 -0
  98. {idemkit-0.1.0 → idemkit-0.1.1}/tests/_backends.py +0 -0
  99. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/__init__.py +0 -0
  100. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_backend_contract.py +0 -0
  101. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_concurrent_claim.py +0 -0
  102. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_max_size.py +0 -0
  103. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_mongo_backend.py +0 -0
  104. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_postgres_backend.py +0 -0
  105. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_postgres_verify_schema.py +0 -0
  106. {idemkit-0.1.0 → idemkit-0.1.1}/tests/backends/test_redis_backend.py +0 -0
  107. {idemkit-0.1.0 → idemkit-0.1.1}/tests/cli/__init__.py +0 -0
  108. {idemkit-0.1.0 → idemkit-0.1.1}/tests/cli/test_cli.py +0 -0
  109. {idemkit-0.1.0 → idemkit-0.1.1}/tests/conformance/__init__.py +0 -0
  110. {idemkit-0.1.0 → idemkit-0.1.1}/tests/conformance/test_conformance_suite.py +0 -0
  111. {idemkit-0.1.0 → idemkit-0.1.1}/tests/conftest.py +0 -0
  112. {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/__init__.py +0 -0
  113. {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_contrib.py +0 -0
  114. {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_drf.py +0 -0
  115. {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_fastapi.py +0 -0
  116. {idemkit-0.1.0 → idemkit-0.1.1}/tests/contrib/test_observability.py +0 -0
  117. {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/__init__.py +0 -0
  118. {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_codecs.py +0 -0
  119. {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_core_runner.py +0 -0
  120. {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_fingerprint.py +0 -0
  121. {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_manual_clock.py +0 -0
  122. {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_policy.py +0 -0
  123. {idemkit-0.1.0 → idemkit-0.1.1}/tests/core/test_sync_bridge.py +0 -0
  124. {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/__init__.py +0 -0
  125. {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/test_clock_skew.py +0 -0
  126. {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/test_fault_injection.py +0 -0
  127. {idemkit-0.1.0 → idemkit-0.1.1}/tests/correctness/test_property_stateful.py +0 -0
  128. {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/__init__.py +0 -0
  129. {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/conftest.py +0 -0
  130. {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/docker-compose.yml +0 -0
  131. {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_http_endpoint.py +0 -0
  132. {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_kafka.py +0 -0
  133. {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_rabbitmq.py +0 -0
  134. {idemkit-0.1.0 → idemkit-0.1.1}/tests/e2e/test_sqs.py +0 -0
  135. {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/__init__.py +0 -0
  136. {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/_examples.py +0 -0
  137. {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_http_examples.py +0 -0
  138. {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_method_examples.py +0 -0
  139. {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_queue_examples.py +0 -0
  140. {idemkit-0.1.0 → idemkit-0.1.1}/tests/examples/test_shared_examples.py +0 -0
  141. {idemkit-0.1.0 → idemkit-0.1.1}/tests/fakes.py +0 -0
  142. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/__init__.py +0 -0
  143. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_asgi_middleware.py +0 -0
  144. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_asgi_with_real_backends.py +0 -0
  145. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_body_size.py +0 -0
  146. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_config.py +0 -0
  147. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_engine.py +0 -0
  148. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_header_filtering.py +0 -0
  149. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_observability.py +0 -0
  150. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_redactor.py +0 -0
  151. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_route_decorator.py +0 -0
  152. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_storage_errors.py +0 -0
  153. {idemkit-0.1.0 → idemkit-0.1.1}/tests/http/test_wsgi_middleware.py +0 -0
  154. {idemkit-0.1.0 → idemkit-0.1.1}/tests/method/__init__.py +0 -0
  155. {idemkit-0.1.0 → idemkit-0.1.1}/tests/method/test_nested_keys.py +0 -0
  156. {idemkit-0.1.0 → idemkit-0.1.1}/tests/queue/__init__.py +0 -0
  157. {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.0
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
+ [![PyPI](https://img.shields.io/pypi/v/idemkit.svg)](https://pypi.org/project/idemkit/)
65
66
  [![CI](https://github.com/idemkit/idemkit/actions/workflows/ci.yml/badge.svg)](https://github.com/idemkit/idemkit/actions/workflows/ci.yml)
66
67
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](../LICENSE)
67
68
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org)
69
+ [![Downloads](https://img.shields.io/pypi/dm/idemkit.svg)](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
- git clone https://github.com/idemkit/idemkit && cd idemkit/python
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
- Once published, this becomes `pip install "idemkit[redis]"` (or `[postgres]`, `[mongo]`, `[dynamodb]`, `[asgi]`).
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 -e ".[asgi]" fastapi uvicorn
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
+ [![PyPI](https://img.shields.io/pypi/v/idemkit.svg)](https://pypi.org/project/idemkit/)
3
4
  [![CI](https://github.com/idemkit/idemkit/actions/workflows/ci.yml/badge.svg)](https://github.com/idemkit/idemkit/actions/workflows/ci.yml)
4
5
  [![License: Apache 2.0](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](../LICENSE)
5
6
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org)
7
+ [![Downloads](https://img.shields.io/pypi/dm/idemkit.svg)](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
- git clone https://github.com/idemkit/idemkit && cd idemkit/python
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
- Once published, this becomes `pip install "idemkit[redis]"` (or `[postgres]`, `[mongo]`, `[dynamodb]`, `[asgi]`).
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 -e ".[asgi]" fastapi uvicorn
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
- {"request_id", "requestid", "nonce", "timestamp", "ts", "idempotency_key"}
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 or lowered.endswith("_id"):
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