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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.
@@ -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))
@@ -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]: ...