python-wrap 0.2.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 (73) hide show
  1. python_wrap-0.2.1/.gitignore +184 -0
  2. python_wrap-0.2.1/CHANGELOG.md +322 -0
  3. python_wrap-0.2.1/LICENSE +21 -0
  4. python_wrap-0.2.1/PKG-INFO +485 -0
  5. python_wrap-0.2.1/README.md +454 -0
  6. python_wrap-0.2.1/benchmarks/run.py +284 -0
  7. python_wrap-0.2.1/examples/README.md +22 -0
  8. python_wrap-0.2.1/examples/async_crawler.py +100 -0
  9. python_wrap-0.2.1/examples/audit_logging.py +86 -0
  10. python_wrap-0.2.1/examples/cli_tools.py +116 -0
  11. python_wrap-0.2.1/examples/dataloader.py +60 -0
  12. python_wrap-0.2.1/examples/resilient_http.py +142 -0
  13. python_wrap-0.2.1/examples/service_layer.py +80 -0
  14. python_wrap-0.2.1/examples/web_caching.py +94 -0
  15. python_wrap-0.2.1/pyproject.toml +121 -0
  16. python_wrap-0.2.1/scripts/check_installed.py +65 -0
  17. python_wrap-0.2.1/src/python_wrap/__init__.py +272 -0
  18. python_wrap-0.2.1/src/python_wrap/_core.py +212 -0
  19. python_wrap-0.2.1/src/python_wrap/aio.py +620 -0
  20. python_wrap-0.2.1/src/python_wrap/batching.py +556 -0
  21. python_wrap-0.2.1/src/python_wrap/bulkhead.py +431 -0
  22. python_wrap-0.2.1/src/python_wrap/caching.py +1613 -0
  23. python_wrap-0.2.1/src/python_wrap/circuit.py +553 -0
  24. python_wrap-0.2.1/src/python_wrap/concurrency.py +1886 -0
  25. python_wrap-0.2.1/src/python_wrap/config.py +217 -0
  26. python_wrap-0.2.1/src/python_wrap/deadline.py +168 -0
  27. python_wrap-0.2.1/src/python_wrap/decorators.py +7 -0
  28. python_wrap-0.2.1/src/python_wrap/devtools.py +385 -0
  29. python_wrap-0.2.1/src/python_wrap/errors.py +86 -0
  30. python_wrap-0.2.1/src/python_wrap/events.py +161 -0
  31. python_wrap-0.2.1/src/python_wrap/hedging.py +192 -0
  32. python_wrap-0.2.1/src/python_wrap/logs.py +767 -0
  33. python_wrap-0.2.1/src/python_wrap/metrics.py +242 -0
  34. python_wrap-0.2.1/src/python_wrap/periodic.py +491 -0
  35. python_wrap-0.2.1/src/python_wrap/policy.py +263 -0
  36. python_wrap-0.2.1/src/python_wrap/py.typed +0 -0
  37. python_wrap-0.2.1/src/python_wrap/retry.py +1324 -0
  38. python_wrap-0.2.1/src/python_wrap/state.py +500 -0
  39. python_wrap-0.2.1/src/python_wrap/testing.py +662 -0
  40. python_wrap-0.2.1/src/python_wrap/timing.py +535 -0
  41. python_wrap-0.2.1/src/python_wrap/validation.py +662 -0
  42. python_wrap-0.2.1/tests/_pickle_helpers.py +298 -0
  43. python_wrap-0.2.1/tests/conftest.py +21 -0
  44. python_wrap-0.2.1/tests/stress/test_thread_safety.py +1188 -0
  45. python_wrap-0.2.1/tests/test_aio.py +521 -0
  46. python_wrap-0.2.1/tests/test_batching.py +612 -0
  47. python_wrap-0.2.1/tests/test_benchmarks_smoke.py +47 -0
  48. python_wrap-0.2.1/tests/test_bulkhead.py +435 -0
  49. python_wrap-0.2.1/tests/test_caching.py +1415 -0
  50. python_wrap-0.2.1/tests/test_caching_extras.py +657 -0
  51. python_wrap-0.2.1/tests/test_circuit.py +471 -0
  52. python_wrap-0.2.1/tests/test_concurrency.py +2028 -0
  53. python_wrap-0.2.1/tests/test_config.py +289 -0
  54. python_wrap-0.2.1/tests/test_core.py +109 -0
  55. python_wrap-0.2.1/tests/test_deadline.py +171 -0
  56. python_wrap-0.2.1/tests/test_devtools.py +574 -0
  57. python_wrap-0.2.1/tests/test_events.py +101 -0
  58. python_wrap-0.2.1/tests/test_examples.py +35 -0
  59. python_wrap-0.2.1/tests/test_hedging.py +339 -0
  60. python_wrap-0.2.1/tests/test_logs.py +777 -0
  61. python_wrap-0.2.1/tests/test_metrics.py +113 -0
  62. python_wrap-0.2.1/tests/test_periodic.py +705 -0
  63. python_wrap-0.2.1/tests/test_pickling.py +175 -0
  64. python_wrap-0.2.1/tests/test_policy.py +154 -0
  65. python_wrap-0.2.1/tests/test_public_api.py +64 -0
  66. python_wrap-0.2.1/tests/test_retry.py +1334 -0
  67. python_wrap-0.2.1/tests/test_state.py +493 -0
  68. python_wrap-0.2.1/tests/test_testing.py +550 -0
  69. python_wrap-0.2.1/tests/test_timing.py +439 -0
  70. python_wrap-0.2.1/tests/test_typing.py +61 -0
  71. python_wrap-0.2.1/tests/test_validation.py +971 -0
  72. python_wrap-0.2.1/tests/typing_cases/decorator_cases.py +359 -0
  73. python_wrap-0.2.1/tests/typing_cases/suppress_cases.py +31 -0
@@ -0,0 +1,184 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+ # Project-specific (explicit, in case the sections above change)
177
+ .venv/
178
+ site/
179
+ dist/
180
+ .ruff_cache/
181
+ .mypy_cache/
182
+ .pytest_cache/
183
+ .coverage
184
+ coverage.xml
@@ -0,0 +1,322 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
6
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ While the version is below `1.0` the public API may change between minor versions.
9
+ Breaking changes are listed under **Breaking changes**, each with its migration.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [0.2.1] - 2026-10-29
14
+
15
+ Publishes the completed 0.2 release under a fresh version, preserving the older
16
+ `v0.2.0` tag from the failed publishing attempt.
17
+
18
+ A ground-up rework and a much larger toolkit. Every decorator works on plain functions,
19
+ `async def` functions and methods, keeps the wrapped signature for type checkers, and never
20
+ configures logging. Several
21
+ v0.1 decorators that did not do what their names promised (`transactional`, `revert_on_failure`,
22
+ async `timing`) now work. See the
23
+ [migration guide](https://pythonwrap.readthedocs.io/en/latest/project/migration/) for the full upgrade
24
+ path.
25
+
26
+ ### Highlights
27
+
28
+ - **Zero dependencies, fully typed.** `wrapt` and `typing-extensions` are gone; signatures are
29
+ preserved with `ParamSpec`, the package ships `py.typed`, and `mypy --strict` is clean.
30
+ - **Sync and async with one API.** `async def` wrappers stay coroutine functions and never block
31
+ the event loop.
32
+ - **A real resilience toolkit.** `retry` with backoff, jitter and hooks; `CircuitBreaker`;
33
+ `fallback`; `suppress`; `timeout` that actually interrupts.
34
+ - **A real concurrency toolkit.** `memoize` with TTL and LRU, `rate_limit` with a sliding window,
35
+ a shared `RateLimiter`, `Bulkhead`, `throttle`, `debounce` and `singleflight`.
36
+ - **Retry that honours the server.** `retry(delay_from=retry_after_header)` follows `Retry-After`,
37
+ `max_time=` and `deadline()` cap the total time, and cancellation is never retried.
38
+ - **An async toolkit.** `gather_limit`, `map_async`, `amap_iter`, `race` and `run_sync`, plus
39
+ `batch_calls` (the DataLoader pattern) and `hedge`.
40
+ - **Policies and configuration.** `policy(...)` applies protections in the recommended order, and
41
+ `configure()` sets project-wide defaults.
42
+ - **Observable and testable.** `count_calls`, one `python_wrap.events` hook for every component,
43
+ `log_context`, and `python_wrap.testing` (`fast_mode`, `FakeClock`, `spy`, pytest fixtures).
44
+ - **Fast.** The `memoize` hit path is about 9x faster and the validation and logging decorators
45
+ skip signature binding on plain calls.
46
+ - **Safe by default.** Importing the package no longer touches the root logger, and `log_args`,
47
+ `audit` and `trace` redact secrets.
48
+ - **Works on Windows**, and is tested on Python 3.10 to 3.14 on Linux, macOS and Windows.
49
+
50
+ ### Breaking changes
51
+
52
+ - **`run_in_thread` returns a `concurrent.futures.Future`** instead of a `Thread`. Migrate:
53
+ replace `.join()` with `.result()` (which also returns the value and re-raises errors).
54
+ - **`benchmark` is a plain function**, `benchmark(func, *, args=(), kwargs=None, runs=5, warmup=1)
55
+ -> BenchmarkResult`, instead of a decorator that silently ran the function three times per
56
+ call. Migrate: call `benchmark(func, args=(...))`; use `benchmark_async` for coroutines.
57
+ - **`retry_on_exception` is deprecated and now stops after 3 attempts** by default (v0.1 retried
58
+ forever with no delay). Migrate: use `retry(on=...)`, and pass `max_attempts=None` to keep the
59
+ old unlimited behaviour.
60
+ - **`rate_limit` raises `RateLimitExceeded`** instead of a bare `Exception`. Migrate: catch
61
+ `RateLimitExceeded` (use `exc.retry_after`), or pass `block=True` to wait instead.
62
+ - **`log_args`, `log_return` and `trace` log at `DEBUG` by default** (`log_args` used `INFO`).
63
+ Migrate: pass `level=logging.INFO` to keep the old level.
64
+ - **`no_debug` is a deprecated no-op** and will be removed in 0.3. Migrate: delete the decorator.
65
+ - **`validate_args` and `check_type` raise `TypeError` at decoration time** for a name that is not
66
+ a parameter of the function. Migrate: fix the misspelled or stale parameter name.
67
+ - **Python 3.8 and 3.9 are no longer supported.** Migrate: upgrade to Python 3.10 or newer, or
68
+ stay on 0.1.0.
69
+ - **Importing the package no longer calls `logging.basicConfig`.** Migrate: configure logging in
70
+ your application if you relied on python-wrap's root-logger setup.
71
+
72
+ ### Added
73
+
74
+ Resilience:
75
+
76
+ - `RetryBudget` (with `RetryBudgetStats`): one Finagle/Envoy-style retry allowance shared
77
+ across functions, threads and event loops via `retry(budget=...)`, `retrying(budget=...)` or
78
+ `configure(retry={"budget": ...})`. When it is spent, retrying stops at once and emits
79
+ `retry.budget_exhausted`, which prevents retry storms against a failing dependency.
80
+ - `retry_if_result=` on `retry`/`retrying` retries when the function returns a matching value;
81
+ once retries run out the last value is returned. `retrying` blocks set `attempt.result`.
82
+ - For `async def` functions, `retry_if`, `retry_if_result` and `on_retry` may be async and are
83
+ awaited; `async with attempt:` is supported in `retrying`.
84
+ - `timeout(..., cleanup_grace=)` bounds async cleanup after the limit: the call is cancelled,
85
+ given the grace to finish its cleanup, then `TimeoutExpired` is raised.
86
+ - `retry`: `delay_from=` (with the `retry_after_header` helper for `Retry-After`), `max_time=`,
87
+ `log_level=` and `log_exc_info=`; `retry_stats(func)` and `RetryStats`; `retrying(...)` /
88
+ `Retrying` for retrying an inline block (`for` / `async for` with `with attempt`); retries
89
+ honour `deadline()`. Cancellation, `KeyboardInterrupt`, `SystemExit` and `GeneratorExit` are
90
+ never retried.
91
+ - `CircuitBreaker`: `success_threshold`, `exclude=`, `on_state_change` and `.stats`
92
+ (`CircuitStats`).
93
+ - `timeout` is also a context manager (`with timeout(5):` / `async with timeout(5):`) and returns
94
+ a `Timeout` object. `python_wrap.deadline` (`deadline()`, `remaining()`, `check()`) gives code a
95
+ shared time budget, and `timeout` clamps to it.
96
+ - `hedge(delay, max_extra=, on=)`: hedged requests for idempotent calls; the first success wins
97
+ and async losers are cancelled.
98
+
99
+ Async toolkit and concurrency:
100
+
101
+ - `periodic(interval, *, jitter=, initial_delay=, max_runs=, on_error=)` runs a sync function in
102
+ a daemon thread, or an async function as a task, every N seconds without drift (missed ticks
103
+ are skipped). The function stays callable and gains `start()`, `stop()`, `running`, `runs`,
104
+ `last_error` and `running_in_background()` for clean shutdown. Events `periodic.run` and
105
+ `periodic.failed`.
106
+ - `python_wrap.aio`: `gather_limit`, `map_async` and `amap_iter` (bounded concurrency, lazy
107
+ input, ordered results; everything is cancelled and awaited on error, cancellation or early
108
+ exit), `race` (first success wins; `RaceFailed`) and `run_sync` (refuses to run inside a running
109
+ loop).
110
+ - `Bulkhead(max_concurrent, max_waiting=, timeout=, name=)` for sync and async (decorator, `with`,
111
+ `async with`, `.active`, `.waiting`), `limit_concurrency(n)` and `BulkheadFull`.
112
+ - `RateLimiter`: one budget shared across functions (`with`, `async with`, `acquire`,
113
+ `try_acquire`), and several windows at once with `rate_limit(limits=[(5, 1), (600, 900)])`.
114
+ - `batch_calls(max_size=, max_wait=, key=)`: the DataLoader pattern for sync and async code, with
115
+ de-duplication, `.flush()` / `.aflush()` / `.pending`, and `BatchError` on contract violations.
116
+
117
+ Caching:
118
+
119
+ - `memoize(ttl=..., stale_ttl=...)`: stale-while-revalidate. After `ttl` a stale value is served
120
+ at once while one background refresh runs (a task for async functions, a daemon thread for
121
+ sync ones); a failed refresh keeps the stale value and is logged. Events `cache.stale` and
122
+ `cache.refresh_failed`.
123
+ - `memoize(store=...)` and the `CacheStore` protocol: keep entries in any store (a dict, Redis,
124
+ ...); the `cache_*` helpers work against it.
125
+ - `memoize(per_instance=True)`: per-instance method caches held through weak references, so
126
+ instances stay collectable and need not be hashable.
127
+ - `cached_property(ttl=)` (thread-safe, `async def` getters, `invalidate()`).
128
+ - `memoize`: `cache_exceptions=` / `exception_ttl=`, a per-item `ttl=` callable, and
129
+ `cache_contains()`, `cache_get()` and `cache_parameters()`.
130
+
131
+ Observability, configuration and testing:
132
+
133
+ - `trace(otel=True, span_name=...)` wraps calls in an OpenTelemetry span (sync and async) with
134
+ `code.function`, `code.namespace` and `python_wrap.context.*` attributes and exception
135
+ recording. `opentelemetry-api` is imported lazily and only when asked for; python-wrap still
136
+ has no runtime dependencies.
137
+ - `count_calls`: calls, errors, in-flight, mean, p50, p95 and p99 per function.
138
+ - `python_wrap.events`: `subscribe()`, `listening()`, `filter_events()` and `EVENT_NAMES`, one hook
139
+ for retry, cache, circuit, limiter, bulkhead and timeout events (Prometheus, OpenTelemetry and
140
+ statsd recipes in the docs).
141
+ - `log_context()` / `current_log_context()`: contextvar fields in `log_args`, `log_return`,
142
+ `audit`, `trace` and `timing` messages, on `LogRecord.context` and on `AuditRecord.context`.
143
+ `log_args` and `audit` accept `redact_extra=`.
144
+ - `policy(...)` / `Policy`: reusable, immutable decorator bundles placed by role (`observe`,
145
+ `validate`, `fallback`, `cache`, `limiter`, `bulkhead`, `breaker`, `retry`, `timeout`, `guard`,
146
+ `outer`, `inner`) in the recommended order, with `.with_()`; `wrap_methods(*decorators,
147
+ include=, exclude=, include_private=)` decorates a class.
148
+ - `python_wrap.config`: `configure()`, `configured()`, `get_config()` and `reset_config()` for
149
+ project-wide defaults (logger, retry options, redaction), read at decoration time; explicit
150
+ arguments win.
151
+ - `python_wrap.testing`: `fast_mode()`, `disable_retries()`, `disable_rate_limits()`,
152
+ `disable_caching()`, `overrides()` (context managers and decorators), `FakeClock` /
153
+ `fake_clock()`, `spy()` / `spy_on()`, `eventually()` / `eventually_sync()`, and the pytest
154
+ fixtures `pw_fake_clock` and `pw_fast_mode` (`pytest_plugins = ["python_wrap.testing"]`).
155
+ - Typing: stacked decorators keep the exact function type (`@retry` over `@memoize` keeps
156
+ `.cache_info()`), and `Memoized`, `Once`, `CircuitProtected`, `Limited` and `Counted` bind
157
+ correctly under methods and `@classmethod`. Typing regression tests run `mypy` in CI.
158
+ - A cookbook of eight recipes, seven runnable `examples/`, a performance page with
159
+ `benchmarks/run.py`, and a thread-safety stress suite (`PW_STRESS=1`, also run on free-threaded
160
+ 3.13t and 3.14t).
161
+
162
+ Core:
163
+
164
+ - Decorated functions, including `periodic` objects and `wrap_methods` classes, pickle by
165
+ reference, so they work with `multiprocessing` and `ProcessPoolExecutor` under the "spawn"
166
+ start method. Cache, limiter and breaker state is per worker process.
167
+ - `retry`: `backoff`, `max_delay`, `jitter`, `on=` exception filter, `retry_if=` predicate,
168
+ `on_retry=` hook receiving a `RetryState`, unlimited attempts (`max_attempts=None`), and bare
169
+ `@retry` usage.
170
+ - `fallback(value)` / `fallback(handler=...)` and `suppress(*exceptions)` for failing soft.
171
+ - `CircuitBreaker` (shareable across functions), the `circuit_breaker` decorator (breaker exposed
172
+ as `.breaker`) and `CircuitState`.
173
+ - `memoize`: `maxsize` (LRU), `ttl`, `typed`, `key=`, `cache_info()`, `cache_clear()`,
174
+ `cache_delete()`, and in-flight de-duplication for async functions.
175
+ - `throttle(interval)`, `debounce(wait)` with `.cancel()` and `.flush()`, `singleflight(key=)`,
176
+ and `rate_limit(block=, key=)`.
177
+ - `timing(threshold=, callback=)` with `TimingRecord`, `measure_memory(callback=)` with
178
+ `MemoryRecord`, `profile(sort=, limit=, callback=)`, and `benchmark_async`.
179
+ - `log_args` secret redaction (`redact=`, `DEFAULT_REDACT`) and `max_length=`; `audit` now yields
180
+ a structured `AuditRecord` with `sink=` and `include_result=`; `trace(limit=)`. Every logging
181
+ decorator accepts `logger=` and `level=`.
182
+ - `enforce_types`: runtime checks from annotations (generics, unions, `Literal`, `Annotated`,
183
+ `NewType` and more) for arguments and return values.
184
+ - `Transaction` with `on_commit` / `on_rollback`, `tx=` injection, `current_transaction()`, and
185
+ sync and async context-manager support.
186
+ - `revert_on_failure(attrs=, deep=, target=)`.
187
+ - `deprecate(since=, removed_in=, alternative=, category=)`, which also works on classes;
188
+ `mock_data(enabled=bool | callable)`; `compose(*decorators)`.
189
+ - Exceptions in `python_wrap.errors`, all subclassing `PythonWrapError`: `TimeoutExpired`
190
+ (also a `TimeoutError`), `RateLimitExceeded`, `CircuitOpenError`, `ValidationError` (also a
191
+ `ValueError`) and `ArgumentTypeError` (also a `TypeError`).
192
+ - `once` gained `.reset()` and `.called`.
193
+ - A `py.typed` marker, and a `NullHandler` on the `python_wrap` logger.
194
+
195
+ ### Changed
196
+
197
+ - `CircuitBreaker`, `RateLimiter`, `Bulkhead`, `RetryBudget`, `Transaction`, `Policy` and
198
+ `testing.FakeClock` raise a clear `TypeError` when pickled or copied (their state belongs to
199
+ one process), instead of an error about a thread lock or a copy that silently shares a lock.
200
+ - `RetryState.exception` can be `None` (when a returned value was rejected by
201
+ `retry_if_result`), and `RetryState` has a new `result` field. A sync function given an async
202
+ retry hook raises `TypeError` when decorated.
203
+ - On free-threaded Python 3.14t new threads inherit the caller's context, so `log_context`
204
+ fields carry over into them (`sys.flags.thread_inherit_context`).
205
+ - The project uses a `src/` layout and Hatchling, and requires Python 3.10 or newer.
206
+ - The runtime dependencies `wrapt` and `typing-extensions` were unused and have been removed.
207
+ - `retry` waits with `asyncio.sleep` for `async def` functions, and re-raises the final
208
+ exception unchanged with its original traceback.
209
+ - `timeout` raises `TimeoutExpired`. Async functions are cancelled with `asyncio.wait_for`; sync
210
+ functions are interrupted with `SIGALRM` on the Unix main thread. The thread fallback used
211
+ elsewhere cannot stop the work, and is now documented.
212
+ - `memoize` and `cache` keys are bound to the function signature with defaults applied, so
213
+ `f(1)`, `f(x=1)` and `f(1, y=<default>)` share one entry; list, dict and set arguments are
214
+ converted to hashable keys.
215
+ - `deprecate` points its warning at the caller and sets `__deprecated__` (PEP 702).
216
+ - `from python_wrap.decorators import ...` still works through a compatibility module.
217
+ - `timeout(...)` now returns a `Timeout` object, which is both a decorator and a context manager.
218
+ Decorator use is unchanged.
219
+ - `retry` never retries cancellation-type exceptions (`asyncio.CancelledError`,
220
+ `KeyboardInterrupt`, `SystemExit`, `GeneratorExit`), even if `on=` lists them.
221
+ - The `redact=` default on `log_args` and `audit` is `None`, which means "the configured value, or
222
+ `DEFAULT_REDACT`". Pass `redact=()` to turn redaction off.
223
+ - `audit` with no `sink` and a disabled log level builds no record at all.
224
+ - `Limited.bulkhead` is a read-only property.
225
+ - Test overrides apply only to backoff waits. `fast_mode()` and `fake_clock()` no longer shorten
226
+ limits on how long to wait: async `Bulkhead`/`limit_concurrency` timeouts, `race(timeout=)`,
227
+ async `hedge` delays and async `batch_calls`/`debounce` windows run on real time, like their
228
+ sync versions and `timeout`.
229
+ - Under `fast_mode()`, a blocking rate limiter (`block=True`) that would wait lets the call
230
+ through at once and still counts it.
231
+
232
+ ### Fixed
233
+
234
+ - Final releases now pass through TestPyPI and an installation check before PyPI.
235
+ Both indexes are verified after upload before creating the GitHub release.
236
+ - Source distributions include the examples and benchmark script used by their tests.
237
+ - Release smoke checks validate metadata, public imports, `py.typed`, retry, memoization
238
+ and async behavior in an isolated installed environment.
239
+
240
+ - `AsyncPeriodic.stop()` no longer swallows a cancellation aimed at the task awaiting it, and
241
+ ends a loop whose run swallows the cancel; `stop()` called inside a run ends the loop as soon
242
+ as that run returns.
243
+ - `timeout(..., cleanup_grace=...)` lets an outside cancellation propagate as `CancelledError`
244
+ (matching `asyncio.timeout`), and waits for and retrieves the child task when the caller is
245
+ cancelled instead of logging "Task exception was never retrieved".
246
+ - `periodic(on_error="raise")` logs an error that `stop()` never collected when `start()` is
247
+ called again, instead of dropping it silently.
248
+ - `revert_on_failure` never restored anything because its snapshot was always empty.
249
+ - `transactional` was a no-op; it now commits and rolls back through a `Transaction`.
250
+ - `memoize` and `cache` built keys from `str(args)`, which collided and ignored keyword
251
+ equivalence. `cache()` also shared a single store across all functions it decorated.
252
+ - `timing` measured about zero seconds for `async def` functions, and now uses `perf_counter`.
253
+ - `measure_memory` used `ru_maxrss`, which is Unix-only and reported the wrong units; it now
254
+ uses `tracemalloc`.
255
+ - The package failed to import on Windows because it imported the Unix-only `resource` module.
256
+ - `audit` reported the wrapper's own frame as the caller.
257
+ - `once` was not thread-safe, and now retries after an exception.
258
+ - `validate_args` rebuilt the function signature on every call.
259
+ - Under `fast_mode()` or `fake_clock()`, async bulkhead and `race` timeouts expired at once and
260
+ `hedge` launched every backup attempt immediately.
261
+ - A blocking `RateLimiter`/`rate_limit(block=True)` busy-spun at full CPU under `fast_mode()`.
262
+ - `timeout`'s thread fallback reported a call as late when it advanced the fake clock (for
263
+ example a `retry` inside it), and a nested signal-based `timeout` did not fire under
264
+ `fake_clock()`.
265
+ - `@transactional` on a function with a positional-only (or `*tx` / `**tx`) `tx` parameter
266
+ crashed at call time; it now raises `TypeError` when decorating.
267
+ - `singleflight`'s default key treated `f(1, b=2)` and `f((1,), (("b", 2),))` as the same call,
268
+ and `memoize` on a callable without an inspectable signature treated `f(1, b=2)` and
269
+ `f(1, 2)` as the same call.
270
+
271
+ ### Performance
272
+
273
+ - Async `memoize` and `cache` hits are about 30-35% faster (one lock instead of two).
274
+ - The `memoize` hit path is about 9x faster.
275
+ - `validate_args`, `check_type`, `enforce_types`, `log_args` and `audit` skip signature binding on
276
+ plain positional calls (5x to 40x faster).
277
+ - `audit` skips building a record when nobody would receive it, and `timing` skips formatting
278
+ when its log level is disabled.
279
+ - Numbers and a benchmark script are on the
280
+ [performance page](https://pythonwrap.readthedocs.io/en/latest/project/performance/).
281
+
282
+ ### Deprecated
283
+
284
+ - `retry_on_exception`: use `retry(on=...)`. Emits a `DeprecationWarning`.
285
+ - `no_debug`: a no-op, to be removed in 0.3.
286
+ - `cache`: soft-deprecated in the docs only (no warning yet); prefer `memoize(ttl=...)`.
287
+
288
+ ### Removed
289
+
290
+ - Support for Python 3.8 and 3.9.
291
+ - Poetry as the build and dependency tool (replaced by Hatchling and uv).
292
+ - The Sphinx documentation (replaced by the new documentation site).
293
+ - The token-based PyPI publish workflow (replaced by tag-driven publishing with Trusted
294
+ Publishing).
295
+
296
+ ### Infrastructure
297
+
298
+ - CI runs ruff, `mypy --strict`, the test suite on Linux, macOS and Windows across Python 3.10 to
299
+ 3.14, the test and thread-safety stress suites on free-threaded Python 3.14t, and a wheel
300
+ check.
301
+ - Tag-driven releases publish to PyPI through Trusted Publishing, with no stored token.
302
+ - The documentation build is checked in CI and hosted on Read the Docs.
303
+ - Added Dependabot, issue and pull request templates, `CONTRIBUTING.md`, `SECURITY.md`,
304
+ `CODE_OF_CONDUCT.md` and `RELEASING.md`.
305
+
306
+ ### Known limitations
307
+
308
+ - The `timeout` thread fallback (Windows, non-main threads) cannot stop the running work.
309
+ - `rate_limit`, `throttle` and `debounce` share state across instances when used on methods
310
+ (`rate_limit` accepts `key=` to split it).
311
+ - `memoize` on methods keeps instances alive; use `maxsize` or `ttl`.
312
+ - Concurrent calls to `measure_memory` share one tracker, and `profile` on an async function
313
+ includes event-loop work.
314
+ - `transactional` injects `tx=` by keyword, so declare `tx` last or keyword-only.
315
+
316
+ ## [0.1.0] - 2025-01-21
317
+
318
+ Initial release.
319
+
320
+ [Unreleased]: https://github.com/LitePy/PythonWrap/compare/v0.2.1...HEAD
321
+ [0.2.1]: https://github.com/LitePy/PythonWrap/compare/v0.1.0...v0.2.1
322
+ [0.1.0]: https://github.com/LitePy/PythonWrap/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PythonWrap Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.