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.
- python_wrap-0.2.1/.gitignore +184 -0
- python_wrap-0.2.1/CHANGELOG.md +322 -0
- python_wrap-0.2.1/LICENSE +21 -0
- python_wrap-0.2.1/PKG-INFO +485 -0
- python_wrap-0.2.1/README.md +454 -0
- python_wrap-0.2.1/benchmarks/run.py +284 -0
- python_wrap-0.2.1/examples/README.md +22 -0
- python_wrap-0.2.1/examples/async_crawler.py +100 -0
- python_wrap-0.2.1/examples/audit_logging.py +86 -0
- python_wrap-0.2.1/examples/cli_tools.py +116 -0
- python_wrap-0.2.1/examples/dataloader.py +60 -0
- python_wrap-0.2.1/examples/resilient_http.py +142 -0
- python_wrap-0.2.1/examples/service_layer.py +80 -0
- python_wrap-0.2.1/examples/web_caching.py +94 -0
- python_wrap-0.2.1/pyproject.toml +121 -0
- python_wrap-0.2.1/scripts/check_installed.py +65 -0
- python_wrap-0.2.1/src/python_wrap/__init__.py +272 -0
- python_wrap-0.2.1/src/python_wrap/_core.py +212 -0
- python_wrap-0.2.1/src/python_wrap/aio.py +620 -0
- python_wrap-0.2.1/src/python_wrap/batching.py +556 -0
- python_wrap-0.2.1/src/python_wrap/bulkhead.py +431 -0
- python_wrap-0.2.1/src/python_wrap/caching.py +1613 -0
- python_wrap-0.2.1/src/python_wrap/circuit.py +553 -0
- python_wrap-0.2.1/src/python_wrap/concurrency.py +1886 -0
- python_wrap-0.2.1/src/python_wrap/config.py +217 -0
- python_wrap-0.2.1/src/python_wrap/deadline.py +168 -0
- python_wrap-0.2.1/src/python_wrap/decorators.py +7 -0
- python_wrap-0.2.1/src/python_wrap/devtools.py +385 -0
- python_wrap-0.2.1/src/python_wrap/errors.py +86 -0
- python_wrap-0.2.1/src/python_wrap/events.py +161 -0
- python_wrap-0.2.1/src/python_wrap/hedging.py +192 -0
- python_wrap-0.2.1/src/python_wrap/logs.py +767 -0
- python_wrap-0.2.1/src/python_wrap/metrics.py +242 -0
- python_wrap-0.2.1/src/python_wrap/periodic.py +491 -0
- python_wrap-0.2.1/src/python_wrap/policy.py +263 -0
- python_wrap-0.2.1/src/python_wrap/py.typed +0 -0
- python_wrap-0.2.1/src/python_wrap/retry.py +1324 -0
- python_wrap-0.2.1/src/python_wrap/state.py +500 -0
- python_wrap-0.2.1/src/python_wrap/testing.py +662 -0
- python_wrap-0.2.1/src/python_wrap/timing.py +535 -0
- python_wrap-0.2.1/src/python_wrap/validation.py +662 -0
- python_wrap-0.2.1/tests/_pickle_helpers.py +298 -0
- python_wrap-0.2.1/tests/conftest.py +21 -0
- python_wrap-0.2.1/tests/stress/test_thread_safety.py +1188 -0
- python_wrap-0.2.1/tests/test_aio.py +521 -0
- python_wrap-0.2.1/tests/test_batching.py +612 -0
- python_wrap-0.2.1/tests/test_benchmarks_smoke.py +47 -0
- python_wrap-0.2.1/tests/test_bulkhead.py +435 -0
- python_wrap-0.2.1/tests/test_caching.py +1415 -0
- python_wrap-0.2.1/tests/test_caching_extras.py +657 -0
- python_wrap-0.2.1/tests/test_circuit.py +471 -0
- python_wrap-0.2.1/tests/test_concurrency.py +2028 -0
- python_wrap-0.2.1/tests/test_config.py +289 -0
- python_wrap-0.2.1/tests/test_core.py +109 -0
- python_wrap-0.2.1/tests/test_deadline.py +171 -0
- python_wrap-0.2.1/tests/test_devtools.py +574 -0
- python_wrap-0.2.1/tests/test_events.py +101 -0
- python_wrap-0.2.1/tests/test_examples.py +35 -0
- python_wrap-0.2.1/tests/test_hedging.py +339 -0
- python_wrap-0.2.1/tests/test_logs.py +777 -0
- python_wrap-0.2.1/tests/test_metrics.py +113 -0
- python_wrap-0.2.1/tests/test_periodic.py +705 -0
- python_wrap-0.2.1/tests/test_pickling.py +175 -0
- python_wrap-0.2.1/tests/test_policy.py +154 -0
- python_wrap-0.2.1/tests/test_public_api.py +64 -0
- python_wrap-0.2.1/tests/test_retry.py +1334 -0
- python_wrap-0.2.1/tests/test_state.py +493 -0
- python_wrap-0.2.1/tests/test_testing.py +550 -0
- python_wrap-0.2.1/tests/test_timing.py +439 -0
- python_wrap-0.2.1/tests/test_typing.py +61 -0
- python_wrap-0.2.1/tests/test_validation.py +971 -0
- python_wrap-0.2.1/tests/typing_cases/decorator_cases.py +359 -0
- 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.
|