blakemere-wraptools 0.1.0__py3-none-any.whl
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.
- blakemere_wraptools-0.1.0.dist-info/METADATA +355 -0
- blakemere_wraptools-0.1.0.dist-info/RECORD +20 -0
- blakemere_wraptools-0.1.0.dist-info/WHEEL +4 -0
- blakemere_wraptools-0.1.0.dist-info/licenses/LICENSE +21 -0
- pydecorators/__init__.py +98 -0
- pydecorators/_core.py +70 -0
- pydecorators/_typing.py +30 -0
- pydecorators/cache_result.py +1225 -0
- pydecorators/circuit_breaker.py +171 -0
- pydecorators/deprecated.py +169 -0
- pydecorators/exceptions.py +47 -0
- pydecorators/log_calls.py +304 -0
- pydecorators/measure_time.py +183 -0
- pydecorators/py.typed +0 -0
- pydecorators/rate_limit.py +138 -0
- pydecorators/redis_backend.py +250 -0
- pydecorators/require_env.py +133 -0
- pydecorators/retry.py +207 -0
- pydecorators/timeout.py +64 -0
- pydecorators/validate_types.py +162 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: blakemere-wraptools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A focused library of useful Python decorators for reliability, caching, rate limiting, timeouts, and developer ergonomics.
|
|
5
|
+
Project-URL: Homepage, https://github.com/RusDavies/pydecorators
|
|
6
|
+
Project-URL: Repository, https://github.com/RusDavies/pydecorators
|
|
7
|
+
Project-URL: Issues, https://github.com/RusDavies/pydecorators/issues
|
|
8
|
+
Author: Russ Davies
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cache,decorators,python,rate-limit,retry,timeout
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
23
|
+
Requires-Dist: mypy>=1.14; extra == 'dev'
|
|
24
|
+
Requires-Dist: pre-commit>=4.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
29
|
+
Requires-Dist: twine>=5.1; extra == 'dev'
|
|
30
|
+
Provides-Extra: redis
|
|
31
|
+
Requires-Dist: redis>=5; extra == 'redis'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# PyDecorators
|
|
35
|
+
|
|
36
|
+
A focused Python library of useful decorators for everyday reliability, caching, rate limiting, timeouts, and developer ergonomics.
|
|
37
|
+
|
|
38
|
+
The goal is to provide small, typed, well-tested decorators that work in scripts, CLIs, services, and libraries without requiring a framework or a dependency shrubbery.
|
|
39
|
+
|
|
40
|
+
## Planned first release
|
|
41
|
+
|
|
42
|
+
The initial `v0.1.0` scope is:
|
|
43
|
+
|
|
44
|
+
- `@deprecated` — implemented
|
|
45
|
+
- `@cache_result` — sync/disk backend implemented
|
|
46
|
+
- `@retry` — implemented
|
|
47
|
+
- `@rate_limit` — implemented
|
|
48
|
+
- `@timeout` — async implementation complete
|
|
49
|
+
- `@log_calls` — implemented
|
|
50
|
+
- `@measure_time` — implemented
|
|
51
|
+
- `@validate_types` — implemented
|
|
52
|
+
- `@require_env` — implemented
|
|
53
|
+
- `@circuit_breaker` — implemented
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
Not published yet. For local development:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python -m pip install -e '.[dev]'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For a built local wheel:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m build
|
|
68
|
+
python -m pip install dist/blakemere_wraptools-0.1.0-py3-none-any.whl
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
Pick the smallest decorator that solves the immediate problem:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pydecorators import retry
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@retry(attempts=3, delay=0.25, backoff=2, exceptions=(ConnectionError, TimeoutError))
|
|
80
|
+
def call_service() -> str:
|
|
81
|
+
return "ok"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then read the per-decorator docs and `docs/composition.md` before stacking decorators together. Decorator soup is still soup, even when typed.
|
|
85
|
+
|
|
86
|
+
## Development status
|
|
87
|
+
|
|
88
|
+
Pre-alpha. The project foundation exists and the first wave of decorators is implemented, including `@deprecated`, `@cache_result`, `@retry`, `@rate_limit`, async `@timeout`, `@log_calls`, `@measure_time`, `@validate_types`, `@require_env`, and `@circuit_breaker`.
|
|
89
|
+
|
|
90
|
+
Warnings use `DeprecationWarning` by default, which Python may hide depending on warning filters. See `docs/deprecated.md` for details.
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python -m venv .venv
|
|
96
|
+
source .venv/bin/activate
|
|
97
|
+
python -m pip install -e '.[dev]'
|
|
98
|
+
ruff check .
|
|
99
|
+
ruff format --check .
|
|
100
|
+
mypy
|
|
101
|
+
python scripts/smoke_imports.py
|
|
102
|
+
python scripts/smoke_examples.py
|
|
103
|
+
pytest
|
|
104
|
+
python -m build
|
|
105
|
+
python scripts/smoke_wheel_install.py
|
|
106
|
+
python scripts/dogfood_local_wheel.py
|
|
107
|
+
python scripts/dogfood_external_project.py
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`pytest` enforces coverage for `pydecorators` with terminal and XML coverage reports.
|
|
111
|
+
|
|
112
|
+
Optional local pre-commit hooks are available:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pre-commit install
|
|
116
|
+
pre-commit run --all-files
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Dogfooding before release
|
|
120
|
+
|
|
121
|
+
Publishing is intentionally paused while the package is dogfooded locally. See `DOGFOOD.md` and run:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
python scripts/dogfood_local_wheel.py
|
|
125
|
+
python scripts/dogfood_external_project.py
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Documentation
|
|
129
|
+
|
|
130
|
+
Start with `docs/index.md`, then use the focused pages when you need specifics:
|
|
131
|
+
|
|
132
|
+
- `docs/PUBLIC_API.md` — public API and compatibility policy
|
|
133
|
+
- `docs/API_DESIGN.md` — broader API design notes
|
|
134
|
+
- `docs/API_REFERENCE.md` — compact public API reference
|
|
135
|
+
- `docs/composition.md` — decorator stacking guidance
|
|
136
|
+
- `docs/exceptions.md` — public exception behavior
|
|
137
|
+
- `docs/security_hardening.md` — cache/logging/validation safety guidance
|
|
138
|
+
- `CONTRIBUTING.md` — new-decorator and documentation-maintenance checklist
|
|
139
|
+
|
|
140
|
+
Executable documentation examples live under `docs/examples/`.
|
|
141
|
+
|
|
142
|
+
## Release process
|
|
143
|
+
|
|
144
|
+
See `RELEASE.md` for the release checklist.
|
|
145
|
+
|
|
146
|
+
## Decorator design docs
|
|
147
|
+
|
|
148
|
+
See `docs/cache_result.md` for the cache decorator design, `docs/retry.md` for retry behavior, `docs/rate_limit.md` for rate limiting, `docs/timeout.md` for async timeout behavior, `docs/log_calls.md` for call logging, `docs/measure_time.md` for timing hooks, `docs/validate_types.md` for lightweight runtime type validation, `docs/require_env.md` for environment checks, `docs/circuit_breaker.md` for circuit-breaker behavior, `docs/composition.md` for stacking guidance, and `docs/API_REFERENCE.md` for a compact API reference.
|
|
149
|
+
|
|
150
|
+
## Decorator overview
|
|
151
|
+
|
|
152
|
+
- `@deprecated`: warn when old APIs are used.
|
|
153
|
+
- `@cache_result`: cache expensive sync results in memory or trusted local disk storage.
|
|
154
|
+
- `@retry`: retry transient failures with explicit policy.
|
|
155
|
+
- `@rate_limit`: enforce in-process sliding-window call limits.
|
|
156
|
+
- `@timeout`: apply async deadlines using `asyncio.wait_for`.
|
|
157
|
+
- `@log_calls`: log calls, durations, exceptions, and optional summaries.
|
|
158
|
+
- `@measure_time`: emit timing data to callbacks, loggers, or metrics hooks.
|
|
159
|
+
- `@validate_types`: shallow runtime checks for simple annotations.
|
|
160
|
+
- `@require_env`: check environment requirements at call time.
|
|
161
|
+
- `@circuit_breaker`: stop hammering a failing dependency until a reset window opens.
|
|
162
|
+
|
|
163
|
+
## Security and operational notes
|
|
164
|
+
|
|
165
|
+
See `docs/security_hardening.md` for the centralized hardening checklist.
|
|
166
|
+
|
|
167
|
+
- Treat `DiskCacheBackend` files as trusted local data. The default pickle serializer must not load untrusted cache databases.
|
|
168
|
+
- Do not put disk cache files in world-writable directories.
|
|
169
|
+
- Keep cache values disposable; use namespaces or clear caches when semantics change.
|
|
170
|
+
- Argument/result logging is opt-in because logs preserve secrets with the enthusiasm of a museum curator.
|
|
171
|
+
- `@validate_types` is not a schema validator or security boundary.
|
|
172
|
+
- `@rate_limit` and `@circuit_breaker` are in-process only; they do not coordinate across workers, containers, or hosts.
|
|
173
|
+
- Environment checks protect configuration mistakes, not secret storage. Use proper secret-management systems for real secrets.
|
|
174
|
+
|
|
175
|
+
## Async support notes
|
|
176
|
+
|
|
177
|
+
- `@deprecated`, `@retry`, `@rate_limit`, `@timeout`, `@log_calls`, `@measure_time`, `@validate_types`, `@require_env`, and `@circuit_breaker` support async callables.
|
|
178
|
+
- `@cache_result` is sync-only for now.
|
|
179
|
+
- `@timeout` is deliberately async-only. Sync timeout strategies based on signals or worker threads have sharp edges, so sync functions currently raise `ConfigurationError` instead of pretending the problem is easy.
|
|
180
|
+
|
|
181
|
+
### Deprecated example
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from pydecorators import deprecated
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@deprecated("Kept for compatibility.", replacement="new_function", version="0.1.0")
|
|
188
|
+
def old_function() -> str:
|
|
189
|
+
return "still works"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Retry example
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from pydecorators import retry
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@retry(attempts=3, delay=0.25, backoff=2, exceptions=ConnectionError)
|
|
199
|
+
def call_service() -> str:
|
|
200
|
+
return "ok"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`@retry` supports sync and async functions, exception filtering, predicate-based retry decisions, attempt hooks, jitter, max-delay caps, and injectable sleep functions for fast tests.
|
|
204
|
+
|
|
205
|
+
### Rate-limit example
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from pydecorators import rate_limit
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@rate_limit(calls=10, period=60, key=lambda user_id: user_id)
|
|
212
|
+
def call_user_api(user_id: str) -> str:
|
|
213
|
+
return "ok"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`@rate_limit` uses an in-process sliding window and supports global or keyed buckets, raise or block mode, async functions, and injectable clocks/sleep functions for tests.
|
|
217
|
+
|
|
218
|
+
### Timeout example
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from pydecorators import timeout
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@timeout(seconds=2)
|
|
225
|
+
async def fetch_user(user_id: str) -> str:
|
|
226
|
+
return user_id
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`@timeout` currently supports async functions using `asyncio.wait_for`. Sync functions are rejected deliberately until a safe, well-documented sync timeout strategy exists.
|
|
230
|
+
|
|
231
|
+
### Logging example
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from pydecorators import log_calls
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@log_calls(include_args=True, redact_args={"password"})
|
|
238
|
+
def authenticate(*, username: str, password: str) -> bool:
|
|
239
|
+
return bool(username and password)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`@log_calls` logs start/finish duration and exceptions by default. Argument and result logging are opt-in; use redaction and summaries carefully because logs are where secrets go to become immortal.
|
|
243
|
+
|
|
244
|
+
### Timing example
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from pydecorators import TimingInfo, measure_time
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
timings: list[TimingInfo] = []
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@measure_time(callback=timings.append)
|
|
254
|
+
def rebuild_index() -> None:
|
|
255
|
+
pass
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`@measure_time` records sync and async durations through optional callback, logger, or metrics hooks.
|
|
259
|
+
|
|
260
|
+
### Type-validation example
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from pydecorators import validate_types
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@validate_types(validate_return=True)
|
|
267
|
+
def double(value: int) -> int:
|
|
268
|
+
return value * 2
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`@validate_types` provides lightweight, shallow runtime checks for simple annotations. It is not a full schema validator; sometimes the boring warning label is the difference between a tool and a liability.
|
|
272
|
+
|
|
273
|
+
### Environment requirement example
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from pydecorators import require_env
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@require_env("API_TOKEN")
|
|
280
|
+
def call_service() -> str:
|
|
281
|
+
return "ok"
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`@require_env` checks variables at call time, so tests and deployment systems can patch environment state after import.
|
|
285
|
+
|
|
286
|
+
### Circuit-breaker example
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from pydecorators import CircuitBreakerOpen, circuit_breaker
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@circuit_breaker(failure_threshold=2, reset_timeout=10)
|
|
293
|
+
def call_vendor_api() -> str:
|
|
294
|
+
return "ok"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
call_vendor_api()
|
|
299
|
+
except CircuitBreakerOpen:
|
|
300
|
+
pass
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`@circuit_breaker` is an in-process breaker with closed, open, and half-open states. Useful, not magic. Architecture remains annoyingly undefeated.
|
|
304
|
+
|
|
305
|
+
### Cache example
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from pydecorators import cache_result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@cache_result(maxsize=128)
|
|
312
|
+
def expensive_lookup(value: str) -> str:
|
|
313
|
+
return value.upper()
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`@cache_result` uses `MemoryCacheBackend` by default and also includes `DiskCacheBackend` for trusted local persistent caches. Future backend work is planned for Redis storage.
|
|
317
|
+
|
|
318
|
+
Shared cache backends can be isolated with `namespace=` when multiple decorated functions use the same backend. For persistent disk caches, treat namespace names and custom key functions as part of the cache file's compatibility contract: changing either one can strand old entries, collide with another decorated function, or return values computed under older semantics. Use explicit namespaces for long-lived caches, keep custom key functions stable across releases, and clear or rotate the cache when function behavior, argument meaning, serializer format, or namespace strategy changes.
|
|
319
|
+
|
|
320
|
+
TTL is fixed from write time by default; pass `refresh_ttl_on_hit=True` to `@cache_result` or a backend when hot entries should use sliding expiry. Fixed TTL is better for predictable freshness and retention: an entry expires at a known time even if it is popular. Sliding TTL is better for expensive hot data that may stay cached while traffic continues, but it can keep stale or sensitive values alive indefinitely unless you also bound cache size, choose conservative TTLs, and clear caches when semantics change. Pass `coalesce_misses=True` to `@cache_result` when duplicate concurrent misses for the same key should share one in-flight computation.
|
|
321
|
+
|
|
322
|
+
A simple cache-versioning recipe is to include an application-controlled version in the namespace, such as `namespace="users:v1"`, and bump it to `users:v2` when cached value shape, authorization assumptions, serializer behavior, or key semantics change. That intentionally abandons old rows without needing to parse or migrate them. For larger applications, keep the version string near the code that defines the cached value contract, not buried in a random decorator where future-you will absolutely forget it.
|
|
323
|
+
|
|
324
|
+
For a trusted local persistent cache, create one `DiskCacheBackend`, pass it to `@cache_result`, and close it when your script or service shuts down:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from pathlib import Path
|
|
328
|
+
|
|
329
|
+
from pydecorators import DiskCacheBackend, cache_namespace, cache_result
|
|
330
|
+
|
|
331
|
+
backend = DiskCacheBackend(
|
|
332
|
+
Path(".cache/pydecorators.sqlite3"),
|
|
333
|
+
ttl=3600,
|
|
334
|
+
maxsize=10_000,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@cache_result(backend=backend, namespace=cache_namespace("users", 1))
|
|
339
|
+
def load_user_display_name(user_id: str) -> str:
|
|
340
|
+
return fetch_user_display_name(user_id)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
print(load_user_display_name("user-123"))
|
|
345
|
+
finally:
|
|
346
|
+
backend.close()
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
For long-running applications, keep the backend for the application lifetime and close it from your normal shutdown hook. For short scoped backend operations that are not decorator-bound, `DiskCacheBackend` also supports `with DiskCacheBackend(...) as backend:`.
|
|
350
|
+
|
|
351
|
+
Do **not** create a decorator-bound `DiskCacheBackend` inside a short `with` block unless all decorated calls happen before the block exits. The context manager closes the backend on exit; later calls through the decorated function raise `CacheBackendClosedError`.
|
|
352
|
+
|
|
353
|
+
Disk backend design lives in `docs/disk_cache_backend.md`; implementation uses SQLite and the cache serializer interface. The default disk payload serializer uses pickle, so cache databases must be treated as trusted local files only — do not load cache DBs from untrusted sources or place them in world-writable directories. For simple JSON-compatible payloads, use `JsonCacheSerializer` instead of pickle when values should be easier to inspect or consume from other languages.
|
|
354
|
+
|
|
355
|
+
`DiskCacheBackend` is intended for single-host local caching. It uses normal SQLite file locking, requests WAL mode by default, and configures a 5000 ms busy timeout to reduce transient `database is locked` failures. Those settings improve local reader/writer behavior, but they do not make it a distributed cache and they do not promise safe cross-host semantics on shared/network filesystems. If multiple processes use the same cache file, expect normal SQLite contention behavior and keep cached values disposable. If you need visibility into rows dropped because of serializer mismatches or corrupt payloads, pass `on_drop=` to `DiskCacheBackend` and log the `DiskCacheDropEvent`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
pydecorators/__init__.py,sha256=Wyk6U2-LfKH-I9KWTuBbOmVlQAfu4Sp3Vf91aYLJfSY,2745
|
|
2
|
+
pydecorators/_core.py,sha256=Xv7izivU8ruoWrWh0u1ndOMOsVmbMUtOEtheMMYaxB0,2192
|
|
3
|
+
pydecorators/_typing.py,sha256=-gsBvCZnJNL3i32s_KZhZ2rnp4avE7fxiQvP7hzWGl0,870
|
|
4
|
+
pydecorators/cache_result.py,sha256=nGx0M6v1MQKOT25khACaSNh-OxLdvdRVaiNxLe7S1e0,43291
|
|
5
|
+
pydecorators/circuit_breaker.py,sha256=2IkYTtlVpfRGPtD6JLzxnOAiDu0bAe6mN988S47sMlk,6061
|
|
6
|
+
pydecorators/deprecated.py,sha256=MxRZYKjwxu5TqQDgWPrWzZnQOD5NvFHS-YkOpjaVbPY,4634
|
|
7
|
+
pydecorators/exceptions.py,sha256=M_I20h18hiWTPGGfXbPuk5qREnu4Rh8srdPcM99o6Hc,1457
|
|
8
|
+
pydecorators/log_calls.py,sha256=ST6p6mkDj3_mYeEFAlHLfxBRShKps1sWCM1yP-1wAe8,9596
|
|
9
|
+
pydecorators/measure_time.py,sha256=2kMIJfbOzDRZHFl8c0_Qr_-Cx5edbjfmXUFae2UA1Zc,6000
|
|
10
|
+
pydecorators/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
pydecorators/rate_limit.py,sha256=s6HPspCkoqRovkcB1_mQEPbDskXBrjQ4-TVNs4lQvpE,4702
|
|
12
|
+
pydecorators/redis_backend.py,sha256=SldKcpNq32-nuwx63ENWp6DID3FEX3_IFH4-RbeoYUw,9068
|
|
13
|
+
pydecorators/require_env.py,sha256=OPId8ETjEDKkJoMWASmRj7pJ_Goa3EeC14vQSqDjmBY,4877
|
|
14
|
+
pydecorators/retry.py,sha256=IDOsbpMFj5bVkUTMKy58sZm1KpiZ8XyA7hIECY97wyk,7419
|
|
15
|
+
pydecorators/timeout.py,sha256=B41N5KBcBS1pR1CvSzGx54QeTskTayqLbT4sD948Nng,2220
|
|
16
|
+
pydecorators/validate_types.py,sha256=7J3pzwhjnhFMvyZJUhXJxBlATLXzZ2neFgaHnnzMNSU,5772
|
|
17
|
+
blakemere_wraptools-0.1.0.dist-info/METADATA,sha256=zfgrtip1sGzQF__ADI_KLvLkwi7vEiaLXIK4jZ_6lzM,14681
|
|
18
|
+
blakemere_wraptools-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
19
|
+
blakemere_wraptools-0.1.0.dist-info/licenses/LICENSE,sha256=f1q4FlcGQ_N11BjVPpGOxPC28rAAHmQlBWyXUy7-zH4,1068
|
|
20
|
+
blakemere_wraptools-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Russ Davies
|
|
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.
|
pydecorators/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Useful decorators for everyday Python projects.
|
|
2
|
+
|
|
3
|
+
The package is intentionally small and boring: reliability, caching, rate limiting,
|
|
4
|
+
timeouts, and developer ergonomics without dragging in a framework-shaped sofa.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydecorators.cache_result import (
|
|
8
|
+
CacheBackend,
|
|
9
|
+
CacheCoalescingInfo,
|
|
10
|
+
CacheInfo,
|
|
11
|
+
CacheSerializer,
|
|
12
|
+
DiskCacheAggregateInspectionReport,
|
|
13
|
+
DiskCacheBackend,
|
|
14
|
+
DiskCacheDropEvent,
|
|
15
|
+
DiskCacheInspectionEntry,
|
|
16
|
+
DiskCacheInspectionReport,
|
|
17
|
+
DiskCacheIntegrityReport,
|
|
18
|
+
DiskCacheMaintenanceReport,
|
|
19
|
+
DiskCacheMetadata,
|
|
20
|
+
DiskCachePreviewContext,
|
|
21
|
+
JsonCacheSerializer,
|
|
22
|
+
MemoryCacheBackend,
|
|
23
|
+
PickleCacheSerializer,
|
|
24
|
+
cache_directory,
|
|
25
|
+
cache_namespace,
|
|
26
|
+
cache_result,
|
|
27
|
+
redact_json_preview,
|
|
28
|
+
)
|
|
29
|
+
from pydecorators.circuit_breaker import CircuitBreakerOpen, CircuitState, circuit_breaker
|
|
30
|
+
from pydecorators.deprecated import deprecated
|
|
31
|
+
from pydecorators.exceptions import (
|
|
32
|
+
CacheBackendClosedError,
|
|
33
|
+
CacheKeyError,
|
|
34
|
+
CacheSerializationError,
|
|
35
|
+
ConfigurationError,
|
|
36
|
+
FunctionTimedOut,
|
|
37
|
+
RateLimitExceeded,
|
|
38
|
+
UnsupportedCacheSchemaVersionError,
|
|
39
|
+
UsefulDecoratorsError,
|
|
40
|
+
ValidationError,
|
|
41
|
+
)
|
|
42
|
+
from pydecorators.log_calls import log_calls
|
|
43
|
+
from pydecorators.measure_time import TimingInfo, measure_time
|
|
44
|
+
from pydecorators.rate_limit import rate_limit
|
|
45
|
+
from pydecorators.redis_backend import RedisCacheBackend, RedisCacheClient
|
|
46
|
+
from pydecorators.require_env import EnvRequirementError, require_env
|
|
47
|
+
from pydecorators.retry import retry
|
|
48
|
+
from pydecorators.timeout import timeout
|
|
49
|
+
from pydecorators.validate_types import validate_types
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"CacheBackend",
|
|
55
|
+
"CacheBackendClosedError",
|
|
56
|
+
"CacheCoalescingInfo",
|
|
57
|
+
"CacheInfo",
|
|
58
|
+
"CacheKeyError",
|
|
59
|
+
"CacheSerializationError",
|
|
60
|
+
"CacheSerializer",
|
|
61
|
+
"CircuitBreakerOpen",
|
|
62
|
+
"CircuitState",
|
|
63
|
+
"ConfigurationError",
|
|
64
|
+
"DiskCacheAggregateInspectionReport",
|
|
65
|
+
"DiskCacheBackend",
|
|
66
|
+
"DiskCacheDropEvent",
|
|
67
|
+
"DiskCacheInspectionEntry",
|
|
68
|
+
"DiskCacheInspectionReport",
|
|
69
|
+
"DiskCacheIntegrityReport",
|
|
70
|
+
"DiskCacheMaintenanceReport",
|
|
71
|
+
"DiskCacheMetadata",
|
|
72
|
+
"DiskCachePreviewContext",
|
|
73
|
+
"EnvRequirementError",
|
|
74
|
+
"FunctionTimedOut",
|
|
75
|
+
"JsonCacheSerializer",
|
|
76
|
+
"MemoryCacheBackend",
|
|
77
|
+
"PickleCacheSerializer",
|
|
78
|
+
"RateLimitExceeded",
|
|
79
|
+
"RedisCacheBackend",
|
|
80
|
+
"RedisCacheClient",
|
|
81
|
+
"TimingInfo",
|
|
82
|
+
"UnsupportedCacheSchemaVersionError",
|
|
83
|
+
"UsefulDecoratorsError",
|
|
84
|
+
"ValidationError",
|
|
85
|
+
"cache_directory",
|
|
86
|
+
"cache_namespace",
|
|
87
|
+
"cache_result",
|
|
88
|
+
"circuit_breaker",
|
|
89
|
+
"deprecated",
|
|
90
|
+
"log_calls",
|
|
91
|
+
"measure_time",
|
|
92
|
+
"rate_limit",
|
|
93
|
+
"redact_json_preview",
|
|
94
|
+
"require_env",
|
|
95
|
+
"retry",
|
|
96
|
+
"timeout",
|
|
97
|
+
"validate_types",
|
|
98
|
+
]
|
pydecorators/_core.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Internal helpers shared by decorator implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Any, TypeGuard, cast
|
|
11
|
+
|
|
12
|
+
from pydecorators._typing import P, R
|
|
13
|
+
from pydecorators.exceptions import ConfigurationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_async_callable(func: object) -> TypeGuard[Callable[..., Awaitable[Any]]]:
|
|
17
|
+
"""Return whether *func* is an async callable.
|
|
18
|
+
|
|
19
|
+
`inspect.iscoroutinefunction` handles normal async functions. The `__call__`
|
|
20
|
+
fallback lets callable objects participate without each decorator needing to
|
|
21
|
+
remember that Python allows functions to wear fake moustaches.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if inspect.iscoroutinefunction(func):
|
|
25
|
+
return True
|
|
26
|
+
if not callable(func):
|
|
27
|
+
return False
|
|
28
|
+
return inspect.iscoroutinefunction(type(func).__call__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def monotonic() -> float:
|
|
32
|
+
"""Return a monotonic timestamp suitable for elapsed-time calculations."""
|
|
33
|
+
|
|
34
|
+
return time.monotonic()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sync_sleep(seconds: float) -> None:
|
|
38
|
+
"""Sleep synchronously for *seconds*. Exists mainly for injection in tests."""
|
|
39
|
+
|
|
40
|
+
time.sleep(seconds)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def async_sleep(seconds: float) -> None:
|
|
44
|
+
"""Sleep asynchronously for *seconds*. Exists mainly for injection in tests."""
|
|
45
|
+
|
|
46
|
+
await asyncio.sleep(seconds)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def require_positive_number(name: str, value: float | int) -> None:
|
|
50
|
+
"""Raise :class:`ConfigurationError` unless *value* is greater than zero."""
|
|
51
|
+
|
|
52
|
+
if value <= 0:
|
|
53
|
+
raise ConfigurationError(f"{name} must be greater than zero")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def require_non_negative_number(name: str, value: float | int) -> None:
|
|
57
|
+
"""Raise :class:`ConfigurationError` unless *value* is zero or greater."""
|
|
58
|
+
|
|
59
|
+
if value < 0:
|
|
60
|
+
raise ConfigurationError(f"{name} must be zero or greater")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def mirror_metadata(wrapper: Callable[P, R], wrapped: Callable[P, R]) -> Callable[P, R]:
|
|
64
|
+
"""Apply standard wrapped-function metadata to *wrapper*.
|
|
65
|
+
|
|
66
|
+
This is a tiny wrapper around :func:`functools.wraps` so decorators can use a
|
|
67
|
+
single convention and tests can assert the convention once.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
return cast(Callable[P, R], wraps(wrapped)(wrapper))
|
pydecorators/_typing.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Shared typing primitives for decorator implementations."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import Any, ParamSpec, Protocol, TypeAlias, TypeVar
|
|
5
|
+
|
|
6
|
+
P = ParamSpec("P")
|
|
7
|
+
R = TypeVar("R")
|
|
8
|
+
|
|
9
|
+
SyncCallable: TypeAlias = Callable[P, R]
|
|
10
|
+
AsyncCallable: TypeAlias = Callable[P, Awaitable[R]]
|
|
11
|
+
AnyCallable: TypeAlias = Callable[..., Any]
|
|
12
|
+
Decorator: TypeAlias = Callable[[SyncCallable[P, R]], SyncCallable[P, R]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Clock(Protocol):
|
|
16
|
+
"""Callable protocol for injectable monotonic clocks."""
|
|
17
|
+
|
|
18
|
+
def __call__(self) -> float: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SyncSleep(Protocol):
|
|
22
|
+
"""Callable protocol for injectable synchronous sleep functions."""
|
|
23
|
+
|
|
24
|
+
def __call__(self, seconds: float) -> None: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AsyncSleep(Protocol):
|
|
28
|
+
"""Callable protocol for injectable asynchronous sleep functions."""
|
|
29
|
+
|
|
30
|
+
def __call__(self, seconds: float) -> Awaitable[None]: ...
|