pyresilience 0.1.0__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.
- pyresilience-0.1.0/.gitignore +81 -0
- pyresilience-0.1.0/CHANGELOG.md +20 -0
- pyresilience-0.1.0/LICENSE +21 -0
- pyresilience-0.1.0/PKG-INFO +225 -0
- pyresilience-0.1.0/README.md +185 -0
- pyresilience-0.1.0/pyproject.toml +96 -0
- pyresilience-0.1.0/src/pyresilience/__init__.py +64 -0
- pyresilience-0.1.0/src/pyresilience/__main__.py +15 -0
- pyresilience-0.1.0/src/pyresilience/_bulkhead.py +67 -0
- pyresilience-0.1.0/src/pyresilience/_cache.py +134 -0
- pyresilience-0.1.0/src/pyresilience/_circuit_breaker.py +72 -0
- pyresilience-0.1.0/src/pyresilience/_compat.py +58 -0
- pyresilience-0.1.0/src/pyresilience/_decorator.py +112 -0
- pyresilience-0.1.0/src/pyresilience/_executor.py +418 -0
- pyresilience-0.1.0/src/pyresilience/_logging.py +156 -0
- pyresilience-0.1.0/src/pyresilience/_presets.py +252 -0
- pyresilience-0.1.0/src/pyresilience/_rate_limiter.py +105 -0
- pyresilience-0.1.0/src/pyresilience/_registry.py +142 -0
- pyresilience-0.1.0/src/pyresilience/_types.py +169 -0
- pyresilience-0.1.0/src/pyresilience/logging.py +5 -0
- pyresilience-0.1.0/src/pyresilience/presets.py +5 -0
- pyresilience-0.1.0/src/pyresilience/py.typed +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
*.egg-info/
|
|
24
|
+
.installed.cfg
|
|
25
|
+
*.egg
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
junit.xml
|
|
49
|
+
|
|
50
|
+
# Translations
|
|
51
|
+
*.mo
|
|
52
|
+
*.pot
|
|
53
|
+
|
|
54
|
+
# Environments
|
|
55
|
+
.env
|
|
56
|
+
.venv
|
|
57
|
+
env/
|
|
58
|
+
venv/
|
|
59
|
+
ENV/
|
|
60
|
+
|
|
61
|
+
# IDE
|
|
62
|
+
.vscode/
|
|
63
|
+
.idea/
|
|
64
|
+
*.swp
|
|
65
|
+
*.swo
|
|
66
|
+
*~
|
|
67
|
+
|
|
68
|
+
# mypy
|
|
69
|
+
.mypy_cache/
|
|
70
|
+
dmypy.json
|
|
71
|
+
dmypy.txt
|
|
72
|
+
|
|
73
|
+
# ruff
|
|
74
|
+
.ruff_cache/
|
|
75
|
+
|
|
76
|
+
# mkdocs
|
|
77
|
+
site/
|
|
78
|
+
|
|
79
|
+
# OS
|
|
80
|
+
.DS_Store
|
|
81
|
+
Thumbs.db
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-03-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `@resilient()` decorator combining retry, timeout, circuit breaker, fallback, and bulkhead
|
|
12
|
+
- Full sync and async support
|
|
13
|
+
- `RetryConfig` with exponential backoff and jitter
|
|
14
|
+
- `TimeoutConfig` for per-call timeouts
|
|
15
|
+
- `CircuitBreakerConfig` with half-open recovery
|
|
16
|
+
- `FallbackConfig` with static values or callable fallbacks
|
|
17
|
+
- `BulkheadConfig` for concurrency limiting
|
|
18
|
+
- Structured event system via `ResilienceEvent` and `ResilienceListener`
|
|
19
|
+
- Type-safe configuration dataclasses
|
|
20
|
+
- Zero runtime dependencies
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ahsan Sheraz
|
|
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,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyresilience
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified resilience patterns for Python — retry, circuit breaker, timeout, fallback, and bulkhead in one decorator.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AhsanSheraz/pyresilience
|
|
6
|
+
Project-URL: Repository, https://github.com/AhsanSheraz/pyresilience
|
|
7
|
+
Project-URL: Issues, https://github.com/AhsanSheraz/pyresilience/issues
|
|
8
|
+
Project-URL: Documentation, https://pyresilience.readthedocs.io/
|
|
9
|
+
Project-URL: Changelog, https://github.com/AhsanSheraz/pyresilience/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Ahsan Sheraz <ahsansheraz@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: async,bulkhead,circuit-breaker,decorator,distributed-systems,fallback,fault-tolerance,microservices,python,reliability,resilience,retry,timeout
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Classifier: Topic :: System :: Networking
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.11.0; extra == 'dev'
|
|
36
|
+
Provides-Extra: fast
|
|
37
|
+
Requires-Dist: orjson>=3.9.0; extra == 'fast'
|
|
38
|
+
Requires-Dist: uvloop>=0.17.0; (sys_platform != 'win32') and extra == 'fast'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# pyresilience
|
|
42
|
+
|
|
43
|
+
[](https://github.com/AhsanSheraz/pyresilience/actions/workflows/ci.yml)
|
|
44
|
+
[](https://pypi.org/project/pyresilience/)
|
|
45
|
+
[](https://pypi.org/project/pyresilience/)
|
|
46
|
+
[](https://opensource.org/licenses/MIT)
|
|
47
|
+
|
|
48
|
+
**Unified resilience patterns for Python** — retry, circuit breaker, timeout, fallback, bulkhead, rate limiter, and cache in one decorator. Python's [Resilience4j](https://resilience4j.readme.io/).
|
|
49
|
+
|
|
50
|
+
## Why?
|
|
51
|
+
|
|
52
|
+
Python has `tenacity` for retry, `pybreaker` for circuit breaking, and `wrapt_timeout_decorator` for timeouts. But combining them means stacking decorators, managing separate configs, and losing visibility across patterns. **pyresilience** unifies everything into a single `@resilient()` decorator with zero dependencies.
|
|
53
|
+
|
|
54
|
+
## Resilience4j Feature Parity
|
|
55
|
+
|
|
56
|
+
| Resilience4j Module | pyresilience | Status |
|
|
57
|
+
|---------------------|-------------|--------|
|
|
58
|
+
| CircuitBreaker | `CircuitBreakerConfig` | Complete |
|
|
59
|
+
| Retry | `RetryConfig` | Complete |
|
|
60
|
+
| Bulkhead | `BulkheadConfig` | Complete |
|
|
61
|
+
| TimeLimiter | `TimeoutConfig` | Complete |
|
|
62
|
+
| RateLimiter | `RateLimiterConfig` | Complete |
|
|
63
|
+
| Cache | `CacheConfig` | Complete |
|
|
64
|
+
| Registry | `ResilienceRegistry` | Complete |
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pyresilience
|
|
70
|
+
|
|
71
|
+
# Optional: faster event loop + JSON serialization
|
|
72
|
+
pip install pyresilience[fast]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from pyresilience import resilient, RetryConfig, TimeoutConfig, CircuitBreakerConfig, FallbackConfig
|
|
79
|
+
|
|
80
|
+
@resilient(
|
|
81
|
+
retry=RetryConfig(max_attempts=3, delay=1.0, backoff_factor=2.0),
|
|
82
|
+
timeout=TimeoutConfig(seconds=10),
|
|
83
|
+
circuit_breaker=CircuitBreakerConfig(failure_threshold=5, recovery_timeout=30),
|
|
84
|
+
fallback=FallbackConfig(handler=lambda e: {"error": str(e), "cached": True}),
|
|
85
|
+
)
|
|
86
|
+
def call_api(endpoint: str) -> dict:
|
|
87
|
+
return requests.get(endpoint).json()
|
|
88
|
+
|
|
89
|
+
# Works with async too
|
|
90
|
+
@resilient(
|
|
91
|
+
retry=RetryConfig(max_attempts=3),
|
|
92
|
+
timeout=TimeoutConfig(seconds=5),
|
|
93
|
+
)
|
|
94
|
+
async def async_call(url: str) -> dict:
|
|
95
|
+
async with aiohttp.ClientSession() as session:
|
|
96
|
+
async with session.get(url) as resp:
|
|
97
|
+
return await resp.json()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Features
|
|
101
|
+
|
|
102
|
+
| Pattern | What it does |
|
|
103
|
+
|---------|-------------|
|
|
104
|
+
| **Retry** | Exponential backoff with jitter, configurable exceptions |
|
|
105
|
+
| **Timeout** | Per-call time limits (thread-based sync, native async) |
|
|
106
|
+
| **Circuit Breaker** | Stop calling failing services, auto-recover via half-open |
|
|
107
|
+
| **Fallback** | Graceful degradation with static values or callables |
|
|
108
|
+
| **Bulkhead** | Concurrency limiting to prevent resource exhaustion |
|
|
109
|
+
| **Rate Limiter** | Token bucket rate limiting (calls per time window) |
|
|
110
|
+
| **Cache** | LRU result caching with TTL to avoid redundant calls |
|
|
111
|
+
|
|
112
|
+
Plus:
|
|
113
|
+
- **Registry** for centralized management of named resilience instances
|
|
114
|
+
- **Event system** for observability (`ResilienceListener`)
|
|
115
|
+
- **Opinionated presets** — `http_policy()`, `db_policy()`, `queue_policy()`, `strict_policy()`
|
|
116
|
+
- **Structured logging** — `JsonEventLogger` and `MetricsCollector`
|
|
117
|
+
- **Zero dependencies** — pure Python stdlib
|
|
118
|
+
- **Optional performance backends** — `uvloop` + `orjson` via `pip install pyresilience[fast]`
|
|
119
|
+
- **Full async support** — auto-detects sync vs async
|
|
120
|
+
- **Type-safe** — strict mypy compatible, `py.typed` marker
|
|
121
|
+
- **Python 3.9+**
|
|
122
|
+
|
|
123
|
+
## Rate Limiter
|
|
124
|
+
|
|
125
|
+
Limit call rate using a token bucket algorithm:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from pyresilience import resilient, RateLimiterConfig
|
|
129
|
+
|
|
130
|
+
@resilient(rate_limiter=RateLimiterConfig(max_calls=10, period=1.0))
|
|
131
|
+
def call_api(endpoint: str) -> dict:
|
|
132
|
+
return requests.get(endpoint).json()
|
|
133
|
+
|
|
134
|
+
# With waiting instead of immediate rejection:
|
|
135
|
+
@resilient(rate_limiter=RateLimiterConfig(max_calls=10, period=1.0, max_wait=5.0))
|
|
136
|
+
async def rate_limited_call() -> dict:
|
|
137
|
+
...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Cache
|
|
141
|
+
|
|
142
|
+
Cache function results with TTL and LRU eviction:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from pyresilience import resilient, CacheConfig
|
|
146
|
+
|
|
147
|
+
@resilient(cache=CacheConfig(max_size=256, ttl=300.0))
|
|
148
|
+
def get_user(user_id: int) -> dict:
|
|
149
|
+
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
|
|
150
|
+
|
|
151
|
+
# Second call with same args returns cached result
|
|
152
|
+
get_user(42) # hits DB
|
|
153
|
+
get_user(42) # returns cached, DB not called
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Registry
|
|
157
|
+
|
|
158
|
+
Share resilience state (circuit breakers, rate limiters) across functions:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from pyresilience import ResilienceRegistry, ResilienceConfig, RetryConfig, CircuitBreakerConfig
|
|
162
|
+
|
|
163
|
+
registry = ResilienceRegistry()
|
|
164
|
+
registry.register("payment-api", ResilienceConfig(
|
|
165
|
+
retry=RetryConfig(max_attempts=3),
|
|
166
|
+
circuit_breaker=CircuitBreakerConfig(failure_threshold=5),
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
@registry.decorator("payment-api")
|
|
170
|
+
async def charge_card(amount: float) -> dict:
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
@registry.decorator("payment-api")
|
|
174
|
+
async def refund_card(amount: float) -> dict:
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
# Both functions share the same circuit breaker —
|
|
178
|
+
# if charge_card trips the circuit, refund_card is also blocked
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Presets
|
|
182
|
+
|
|
183
|
+
Opinionated defaults for common integration patterns:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from pyresilience import resilient
|
|
187
|
+
from pyresilience.presets import http_policy, db_policy, queue_policy
|
|
188
|
+
|
|
189
|
+
@resilient(**http_policy())
|
|
190
|
+
def call_api(url: str) -> dict:
|
|
191
|
+
return requests.get(url).json()
|
|
192
|
+
|
|
193
|
+
@resilient(**db_policy())
|
|
194
|
+
def query_db(sql: str) -> list:
|
|
195
|
+
return cursor.execute(sql).fetchall()
|
|
196
|
+
|
|
197
|
+
@resilient(**queue_policy())
|
|
198
|
+
async def publish_message(msg: dict) -> None:
|
|
199
|
+
await producer.send(msg)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Observability
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from pyresilience import resilient, RetryConfig, JsonEventLogger, MetricsCollector
|
|
206
|
+
|
|
207
|
+
logger = JsonEventLogger()
|
|
208
|
+
metrics = MetricsCollector()
|
|
209
|
+
|
|
210
|
+
@resilient(retry=RetryConfig(max_attempts=3), listeners=[logger, metrics])
|
|
211
|
+
def monitored_call():
|
|
212
|
+
return do_work()
|
|
213
|
+
|
|
214
|
+
# After calls:
|
|
215
|
+
print(metrics.summary())
|
|
216
|
+
# {'total_events': 5, 'success_rate': 0.8, 'p99_latency': 1.23, ...}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Documentation
|
|
220
|
+
|
|
221
|
+
Full docs at [pyresilience.readthedocs.io](https://pyresilience.readthedocs.io/)
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# pyresilience
|
|
2
|
+
|
|
3
|
+
[](https://github.com/AhsanSheraz/pyresilience/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pyresilience/)
|
|
5
|
+
[](https://pypi.org/project/pyresilience/)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**Unified resilience patterns for Python** — retry, circuit breaker, timeout, fallback, bulkhead, rate limiter, and cache in one decorator. Python's [Resilience4j](https://resilience4j.readme.io/).
|
|
9
|
+
|
|
10
|
+
## Why?
|
|
11
|
+
|
|
12
|
+
Python has `tenacity` for retry, `pybreaker` for circuit breaking, and `wrapt_timeout_decorator` for timeouts. But combining them means stacking decorators, managing separate configs, and losing visibility across patterns. **pyresilience** unifies everything into a single `@resilient()` decorator with zero dependencies.
|
|
13
|
+
|
|
14
|
+
## Resilience4j Feature Parity
|
|
15
|
+
|
|
16
|
+
| Resilience4j Module | pyresilience | Status |
|
|
17
|
+
|---------------------|-------------|--------|
|
|
18
|
+
| CircuitBreaker | `CircuitBreakerConfig` | Complete |
|
|
19
|
+
| Retry | `RetryConfig` | Complete |
|
|
20
|
+
| Bulkhead | `BulkheadConfig` | Complete |
|
|
21
|
+
| TimeLimiter | `TimeoutConfig` | Complete |
|
|
22
|
+
| RateLimiter | `RateLimiterConfig` | Complete |
|
|
23
|
+
| Cache | `CacheConfig` | Complete |
|
|
24
|
+
| Registry | `ResilienceRegistry` | Complete |
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install pyresilience
|
|
30
|
+
|
|
31
|
+
# Optional: faster event loop + JSON serialization
|
|
32
|
+
pip install pyresilience[fast]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pyresilience import resilient, RetryConfig, TimeoutConfig, CircuitBreakerConfig, FallbackConfig
|
|
39
|
+
|
|
40
|
+
@resilient(
|
|
41
|
+
retry=RetryConfig(max_attempts=3, delay=1.0, backoff_factor=2.0),
|
|
42
|
+
timeout=TimeoutConfig(seconds=10),
|
|
43
|
+
circuit_breaker=CircuitBreakerConfig(failure_threshold=5, recovery_timeout=30),
|
|
44
|
+
fallback=FallbackConfig(handler=lambda e: {"error": str(e), "cached": True}),
|
|
45
|
+
)
|
|
46
|
+
def call_api(endpoint: str) -> dict:
|
|
47
|
+
return requests.get(endpoint).json()
|
|
48
|
+
|
|
49
|
+
# Works with async too
|
|
50
|
+
@resilient(
|
|
51
|
+
retry=RetryConfig(max_attempts=3),
|
|
52
|
+
timeout=TimeoutConfig(seconds=5),
|
|
53
|
+
)
|
|
54
|
+
async def async_call(url: str) -> dict:
|
|
55
|
+
async with aiohttp.ClientSession() as session:
|
|
56
|
+
async with session.get(url) as resp:
|
|
57
|
+
return await resp.json()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
| Pattern | What it does |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| **Retry** | Exponential backoff with jitter, configurable exceptions |
|
|
65
|
+
| **Timeout** | Per-call time limits (thread-based sync, native async) |
|
|
66
|
+
| **Circuit Breaker** | Stop calling failing services, auto-recover via half-open |
|
|
67
|
+
| **Fallback** | Graceful degradation with static values or callables |
|
|
68
|
+
| **Bulkhead** | Concurrency limiting to prevent resource exhaustion |
|
|
69
|
+
| **Rate Limiter** | Token bucket rate limiting (calls per time window) |
|
|
70
|
+
| **Cache** | LRU result caching with TTL to avoid redundant calls |
|
|
71
|
+
|
|
72
|
+
Plus:
|
|
73
|
+
- **Registry** for centralized management of named resilience instances
|
|
74
|
+
- **Event system** for observability (`ResilienceListener`)
|
|
75
|
+
- **Opinionated presets** — `http_policy()`, `db_policy()`, `queue_policy()`, `strict_policy()`
|
|
76
|
+
- **Structured logging** — `JsonEventLogger` and `MetricsCollector`
|
|
77
|
+
- **Zero dependencies** — pure Python stdlib
|
|
78
|
+
- **Optional performance backends** — `uvloop` + `orjson` via `pip install pyresilience[fast]`
|
|
79
|
+
- **Full async support** — auto-detects sync vs async
|
|
80
|
+
- **Type-safe** — strict mypy compatible, `py.typed` marker
|
|
81
|
+
- **Python 3.9+**
|
|
82
|
+
|
|
83
|
+
## Rate Limiter
|
|
84
|
+
|
|
85
|
+
Limit call rate using a token bucket algorithm:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from pyresilience import resilient, RateLimiterConfig
|
|
89
|
+
|
|
90
|
+
@resilient(rate_limiter=RateLimiterConfig(max_calls=10, period=1.0))
|
|
91
|
+
def call_api(endpoint: str) -> dict:
|
|
92
|
+
return requests.get(endpoint).json()
|
|
93
|
+
|
|
94
|
+
# With waiting instead of immediate rejection:
|
|
95
|
+
@resilient(rate_limiter=RateLimiterConfig(max_calls=10, period=1.0, max_wait=5.0))
|
|
96
|
+
async def rate_limited_call() -> dict:
|
|
97
|
+
...
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Cache
|
|
101
|
+
|
|
102
|
+
Cache function results with TTL and LRU eviction:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from pyresilience import resilient, CacheConfig
|
|
106
|
+
|
|
107
|
+
@resilient(cache=CacheConfig(max_size=256, ttl=300.0))
|
|
108
|
+
def get_user(user_id: int) -> dict:
|
|
109
|
+
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
|
|
110
|
+
|
|
111
|
+
# Second call with same args returns cached result
|
|
112
|
+
get_user(42) # hits DB
|
|
113
|
+
get_user(42) # returns cached, DB not called
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Registry
|
|
117
|
+
|
|
118
|
+
Share resilience state (circuit breakers, rate limiters) across functions:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from pyresilience import ResilienceRegistry, ResilienceConfig, RetryConfig, CircuitBreakerConfig
|
|
122
|
+
|
|
123
|
+
registry = ResilienceRegistry()
|
|
124
|
+
registry.register("payment-api", ResilienceConfig(
|
|
125
|
+
retry=RetryConfig(max_attempts=3),
|
|
126
|
+
circuit_breaker=CircuitBreakerConfig(failure_threshold=5),
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
@registry.decorator("payment-api")
|
|
130
|
+
async def charge_card(amount: float) -> dict:
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
@registry.decorator("payment-api")
|
|
134
|
+
async def refund_card(amount: float) -> dict:
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
# Both functions share the same circuit breaker —
|
|
138
|
+
# if charge_card trips the circuit, refund_card is also blocked
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Presets
|
|
142
|
+
|
|
143
|
+
Opinionated defaults for common integration patterns:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from pyresilience import resilient
|
|
147
|
+
from pyresilience.presets import http_policy, db_policy, queue_policy
|
|
148
|
+
|
|
149
|
+
@resilient(**http_policy())
|
|
150
|
+
def call_api(url: str) -> dict:
|
|
151
|
+
return requests.get(url).json()
|
|
152
|
+
|
|
153
|
+
@resilient(**db_policy())
|
|
154
|
+
def query_db(sql: str) -> list:
|
|
155
|
+
return cursor.execute(sql).fetchall()
|
|
156
|
+
|
|
157
|
+
@resilient(**queue_policy())
|
|
158
|
+
async def publish_message(msg: dict) -> None:
|
|
159
|
+
await producer.send(msg)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Observability
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from pyresilience import resilient, RetryConfig, JsonEventLogger, MetricsCollector
|
|
166
|
+
|
|
167
|
+
logger = JsonEventLogger()
|
|
168
|
+
metrics = MetricsCollector()
|
|
169
|
+
|
|
170
|
+
@resilient(retry=RetryConfig(max_attempts=3), listeners=[logger, metrics])
|
|
171
|
+
def monitored_call():
|
|
172
|
+
return do_work()
|
|
173
|
+
|
|
174
|
+
# After calls:
|
|
175
|
+
print(metrics.summary())
|
|
176
|
+
# {'total_events': 5, 'success_rate': 0.8, 'p99_latency': 1.23, ...}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Documentation
|
|
180
|
+
|
|
181
|
+
Full docs at [pyresilience.readthedocs.io](https://pyresilience.readthedocs.io/)
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyresilience"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Unified resilience patterns for Python — retry, circuit breaker, timeout, fallback, and bulkhead in one decorator."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Ahsan Sheraz", email = "ahsansheraz@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"python", "resilience", "retry", "circuit-breaker", "timeout",
|
|
17
|
+
"fallback", "bulkhead", "fault-tolerance", "decorator", "async",
|
|
18
|
+
"reliability", "microservices", "distributed-systems",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Environment :: Console",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
|
+
"Topic :: System :: Networking",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/AhsanSheraz/pyresilience"
|
|
40
|
+
Repository = "https://github.com/AhsanSheraz/pyresilience"
|
|
41
|
+
Issues = "https://github.com/AhsanSheraz/pyresilience/issues"
|
|
42
|
+
Documentation = "https://pyresilience.readthedocs.io/"
|
|
43
|
+
Changelog = "https://github.com/AhsanSheraz/pyresilience/blob/main/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/pyresilience"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = ["src/pyresilience", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
50
|
+
|
|
51
|
+
[project.optional-dependencies]
|
|
52
|
+
fast = [
|
|
53
|
+
"uvloop>=0.17.0; sys_platform != 'win32'",
|
|
54
|
+
"orjson>=3.9.0",
|
|
55
|
+
]
|
|
56
|
+
dev = [
|
|
57
|
+
"pytest>=7.0",
|
|
58
|
+
"pytest-cov>=4.0",
|
|
59
|
+
"pytest-asyncio>=0.21.0",
|
|
60
|
+
"mypy>=1.0",
|
|
61
|
+
"ruff>=0.11.0",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
asyncio_mode = "auto"
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.9"
|
|
70
|
+
strict = true
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
target-version = "py39"
|
|
74
|
+
line-length = 100
|
|
75
|
+
src = ["src", "tests"]
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
select = [
|
|
79
|
+
"E", # pycodestyle errors
|
|
80
|
+
"W", # pycodestyle warnings
|
|
81
|
+
"F", # pyflakes
|
|
82
|
+
"I", # isort
|
|
83
|
+
"UP", # pyupgrade
|
|
84
|
+
"B", # flake8-bugbear
|
|
85
|
+
"SIM", # flake8-simplify
|
|
86
|
+
"TCH", # flake8-type-checking
|
|
87
|
+
]
|
|
88
|
+
ignore = [
|
|
89
|
+
"UP006", # Use `type` instead of `Type` (3.9 compat)
|
|
90
|
+
"UP007", # Use X | Y for union types (3.9 compat)
|
|
91
|
+
"UP035", # Import from `collections.abc` (3.9 compat)
|
|
92
|
+
"UP045", # Use X | None instead of Optional (3.9 compat)
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint.isort]
|
|
96
|
+
known-first-party = ["pyresilience"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""pyresilience — Unified resilience patterns for Python.
|
|
2
|
+
|
|
3
|
+
One decorator to combine retry, timeout, circuit breaker, fallback, and bulkhead.
|
|
4
|
+
Defines the full safety policy for a dependency, not just retries.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pyresilience._bulkhead import BulkheadFullError
|
|
8
|
+
from pyresilience._cache import AsyncResultCache, ResultCache
|
|
9
|
+
from pyresilience._circuit_breaker import CircuitBreaker
|
|
10
|
+
from pyresilience._compat import has_orjson, has_uvloop, install_uvloop
|
|
11
|
+
from pyresilience._decorator import resilient
|
|
12
|
+
from pyresilience._logging import JsonEventLogger, MetricsCollector
|
|
13
|
+
from pyresilience._presets import db_policy, http_policy, queue_policy, strict_policy
|
|
14
|
+
from pyresilience._rate_limiter import AsyncRateLimiter, RateLimiter, RateLimitExceededError
|
|
15
|
+
from pyresilience._registry import ResilienceRegistry
|
|
16
|
+
from pyresilience._types import (
|
|
17
|
+
BulkheadConfig,
|
|
18
|
+
CacheConfig,
|
|
19
|
+
CircuitBreakerConfig,
|
|
20
|
+
CircuitState,
|
|
21
|
+
EventType,
|
|
22
|
+
FallbackConfig,
|
|
23
|
+
RateLimiterConfig,
|
|
24
|
+
ResilienceConfig,
|
|
25
|
+
ResilienceEvent,
|
|
26
|
+
ResilienceListener,
|
|
27
|
+
RetryConfig,
|
|
28
|
+
TimeoutConfig,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"AsyncRateLimiter",
|
|
35
|
+
"AsyncResultCache",
|
|
36
|
+
"BulkheadConfig",
|
|
37
|
+
"BulkheadFullError",
|
|
38
|
+
"CacheConfig",
|
|
39
|
+
"CircuitBreaker",
|
|
40
|
+
"CircuitBreakerConfig",
|
|
41
|
+
"CircuitState",
|
|
42
|
+
"EventType",
|
|
43
|
+
"FallbackConfig",
|
|
44
|
+
"JsonEventLogger",
|
|
45
|
+
"MetricsCollector",
|
|
46
|
+
"RateLimitExceededError",
|
|
47
|
+
"RateLimiter",
|
|
48
|
+
"RateLimiterConfig",
|
|
49
|
+
"ResilienceConfig",
|
|
50
|
+
"ResilienceEvent",
|
|
51
|
+
"ResilienceListener",
|
|
52
|
+
"ResilienceRegistry",
|
|
53
|
+
"ResultCache",
|
|
54
|
+
"RetryConfig",
|
|
55
|
+
"TimeoutConfig",
|
|
56
|
+
"db_policy",
|
|
57
|
+
"has_orjson",
|
|
58
|
+
"has_uvloop",
|
|
59
|
+
"http_policy",
|
|
60
|
+
"install_uvloop",
|
|
61
|
+
"queue_policy",
|
|
62
|
+
"resilient",
|
|
63
|
+
"strict_policy",
|
|
64
|
+
]
|