Skip to content

Commit 0849b73

Browse files
docs: Add comprehensive MkDocs documentation (#39)
* docs: Start building docs * docs: Add comprehensive MkDocs documentation - Enhanced mkdocs.yml with structured navigation - Created complete documentation structure: - Getting started guide with examples - User guide covering decorators, wait strategies, configuration, event handlers, logging, and async - Advanced topics: combining decorators, runtime config, custom strategies - Comprehensive examples for real-world use cases - FAQ with common questions and troubleshooting - API reference with mkdocstrings integration - Added GitHub Actions workflow for automatic deployment to GitHub Pages - Builds upon PR #33 with full content implementation This provides production-ready documentation for the backoff library. * Pin actions * Use locked dependencies * Update changelog --------- Co-authored-by: Edgar Ramírez Mondragón <[email protected]>
1 parent b552afa commit 0849b73

20 files changed

+4490
-19
lines changed

.github/workflows/docs.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Deploy Documentation
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
env:
12+
FORCE_COLOR: 1
13+
UV_NO_SYNC: 1
14+
15+
permissions:
16+
contents: write
17+
18+
jobs:
19+
deploy:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
23+
24+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
25+
26+
- name: Install uv
27+
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
28+
29+
- name: Install dependencies
30+
run: |
31+
uv sync --group docs
32+
33+
- name: Build documentation
34+
run: uv run mkdocs build
35+
36+
- name: Deploy to GitHub Pages
37+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
38+
run: uv run mkdocs gh-deploy --force

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ dist/
1010
.vscode
1111
.tox/
1212
coverage.xml
13+
14+
# mkdocs
15+
site/

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Include tests in source distribution https://github.com/python-backoff/backoff/pull/13
1010
- Added `logging.LoggerAdapter` to `_MaybeLogger` union https://github.com/python-backoff/backoff/pull/34 from @jcbertin
1111
- Add `exception` to the typing-only `Details` dictionary for cases when `on_exception` is used https://github.com/python-backoff/backoff/pull/35
12+
- Add GitHub Actions for CI, documentation, and publishing https://github.com/python-backoff/backoff/pull/39 from @tysoncung
1213

1314
### Packaging
1415

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Combining Decorators
2+
3+
Stack multiple backoff decorators for complex retry logic.
4+
5+
## Basics
6+
7+
Decorators are applied from bottom to top (inside out):
8+
9+
```python
10+
@backoff.on_predicate(backoff.fibo, lambda x: x is None) # Applied last
11+
@backoff.on_exception(backoff.expo, HTTPError) # Applied second
12+
@backoff.on_exception(backoff.expo, Timeout) # Applied first
13+
def complex_operation():
14+
pass
15+
```
16+
17+
## Common Patterns
18+
19+
### Different Exceptions, Different Strategies
20+
21+
```python
22+
@backoff.on_exception(
23+
backoff.expo,
24+
requests.exceptions.Timeout,
25+
max_time=300 # Generous timeout for network issues
26+
)
27+
@backoff.on_exception(
28+
backoff.expo,
29+
requests.exceptions.HTTPError,
30+
max_time=60, # Shorter timeout for HTTP errors
31+
giveup=lambda e: 400 <= e.response.status_code < 500
32+
)
33+
def api_call(url):
34+
response = requests.get(url)
35+
response.raise_for_status()
36+
return response.json()
37+
```
38+
39+
### Exception + Predicate
40+
41+
```python
42+
@backoff.on_predicate(
43+
backoff.constant,
44+
lambda result: result.get("status") == "pending",
45+
interval=5,
46+
max_time=600
47+
)
48+
@backoff.on_exception(
49+
backoff.expo,
50+
requests.exceptions.RequestException,
51+
max_time=60
52+
)
53+
def poll_until_ready(job_id):
54+
response = requests.get(f"/api/jobs/{job_id}")
55+
response.raise_for_status()
56+
return response.json()
57+
```
58+
59+
## Execution Order
60+
61+
Inner decorators execute first:
62+
63+
```python
64+
calls = []
65+
66+
def track_call(func_name):
67+
def handler(details):
68+
calls.append(func_name)
69+
return handler
70+
71+
@backoff.on_exception(
72+
backoff.constant,
73+
ValueError,
74+
on_backoff=track_call('outer'),
75+
max_tries=2,
76+
interval=0.01
77+
)
78+
@backoff.on_exception(
79+
backoff.constant,
80+
TypeError,
81+
on_backoff=track_call('inner'),
82+
max_tries=2,
83+
interval=0.01
84+
)
85+
def failing_function(error_type):
86+
raise error_type("Test")
87+
```
88+
89+
- If `TypeError` raised: inner decorator retries
90+
- If `ValueError` raised: outer decorator retries
91+
- Both errors: inner handles TypeError, then outer handles ValueError
92+
93+
## Best Practices
94+
95+
### Specific Before General
96+
97+
```python
98+
@backoff.on_exception(backoff.expo, Exception) # Catch-all
99+
@backoff.on_exception(backoff.fibo, ConnectionError) # Specific
100+
def network_operation():
101+
pass
102+
```
103+
104+
### Short Timeouts Inside, Long Outside
105+
106+
```python
107+
@backoff.on_exception(
108+
backoff.expo,
109+
Exception,
110+
max_time=600 # Overall 10-minute limit
111+
)
112+
@backoff.on_exception(
113+
backoff.constant,
114+
Timeout,
115+
interval=1,
116+
max_tries=3 # Quick retries for timeouts
117+
)
118+
def layered_retry():
119+
pass
120+
```

docs/advanced/custom-strategies.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Custom Wait Strategies
2+
3+
Create custom wait generators for specialized retry patterns.
4+
5+
## Wait Generator Interface
6+
7+
A wait generator is a function that yields wait times in seconds:
8+
9+
```python
10+
def my_wait_gen():
11+
"""Yields: 1, 2, 3, 4, 5, 5, 5, ..."""
12+
for i in range(1, 6):
13+
yield i
14+
while True:
15+
yield 5
16+
17+
@backoff.on_exception(my_wait_gen, Exception)
18+
def my_function():
19+
pass
20+
```
21+
22+
## Parameters
23+
24+
Accept parameters to customize behavior:
25+
26+
```python
27+
def linear_backoff(start=1, increment=1, max_value=None):
28+
"""Linear backoff: start, start+increment, start+2*increment, ..."""
29+
value = start
30+
while True:
31+
if max_value and value > max_value:
32+
yield max_value
33+
else:
34+
yield value
35+
value += increment
36+
37+
@backoff.on_exception(
38+
linear_backoff,
39+
Exception,
40+
start=2,
41+
increment=3,
42+
max_value=30
43+
)
44+
def my_function():
45+
pass
46+
```
47+
48+
## Examples
49+
50+
### Polynomial Backoff
51+
52+
```python
53+
def polynomial_backoff(base=2, exponent=2, max_value=None):
54+
"""Polynomial: base^(tries^exponent)"""
55+
n = 1
56+
while True:
57+
value = base ** (n ** exponent)
58+
if max_value and value > max_value:
59+
yield max_value
60+
else:
61+
yield value
62+
n += 1
63+
64+
@backoff.on_exception(polynomial_backoff, Exception, base=2, exponent=1.5)
65+
```
66+
67+
### Stepped Backoff
68+
69+
```python
70+
def stepped_backoff(steps):
71+
"""Different wait times for different ranges
72+
steps = [(3, 1), (5, 5), (None, 10)] # 3 tries at 1s, next 5 at 5s, rest at 10s
73+
"""
74+
for max_tries, wait_time in steps:
75+
if max_tries is None:
76+
while True:
77+
yield wait_time
78+
else:
79+
for _ in range(max_tries):
80+
yield wait_time
81+
82+
@backoff.on_exception(
83+
stepped_backoff,
84+
Exception,
85+
steps=[(3, 1), (3, 5), (None, 30)]
86+
)
87+
```
88+
89+
### Random Backoff
90+
91+
```python
92+
import random
93+
94+
def random_backoff(min_wait=1, max_wait=60):
95+
"""Random wait between min and max"""
96+
while True:
97+
yield random.uniform(min_wait, max_wait)
98+
99+
@backoff.on_exception(random_backoff, Exception, min_wait=1, max_wait=10)
100+
```
101+
102+
### Time-of-Day Aware
103+
104+
```python
105+
from datetime import datetime
106+
107+
def business_hours_backoff():
108+
"""Shorter waits during business hours"""
109+
while True:
110+
hour = datetime.now().hour
111+
if 9 <= hour < 17:
112+
yield 5 # 5 seconds during business hours
113+
else:
114+
yield 60 # 1 minute otherwise
115+
116+
@backoff.on_exception(business_hours_backoff, Exception)
117+
```

docs/advanced/runtime-config.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Runtime Configuration
2+
3+
Configure backoff behavior dynamically at runtime.
4+
5+
## Overview
6+
7+
Decorator parameters can accept callables that are evaluated at runtime, allowing dynamic configuration based on application state, environment variables, or configuration files.
8+
9+
## Basic Pattern
10+
11+
```python
12+
class Config:
13+
MAX_RETRIES = 5
14+
MAX_TIME = 60
15+
16+
@backoff.on_exception(
17+
backoff.expo,
18+
Exception,
19+
max_tries=lambda: Config.MAX_RETRIES,
20+
max_time=lambda: Config.MAX_TIME
21+
)
22+
def configurable_function():
23+
pass
24+
25+
# Change configuration at runtime
26+
Config.MAX_RETRIES = 10
27+
```
28+
29+
## Environment Variables
30+
31+
```python
32+
import os
33+
34+
@backoff.on_exception(
35+
backoff.expo,
36+
Exception,
37+
max_tries=lambda: int(os.getenv('RETRY_MAX_TRIES', '5')),
38+
max_time=lambda: int(os.getenv('RETRY_MAX_TIME', '60'))
39+
)
40+
def env_configured():
41+
pass
42+
```
43+
44+
## Configuration Files
45+
46+
```python
47+
import json
48+
49+
def load_config():
50+
with open('config.json') as f:
51+
return json.load(f)
52+
53+
@backoff.on_exception(
54+
backoff.expo,
55+
Exception,
56+
max_tries=lambda: load_config()['retry']['max_tries'],
57+
max_time=lambda: load_config()['retry']['max_time']
58+
)
59+
def file_configured():
60+
pass
61+
```
62+
63+
## Dynamic Wait Strategies
64+
65+
```python
66+
def get_wait_gen():
67+
if app.config.get('fast_retry'):
68+
return backoff.constant
69+
return backoff.expo
70+
71+
@backoff.on_exception(
72+
lambda: get_wait_gen(),
73+
Exception
74+
)
75+
def dynamic_wait():
76+
pass
77+
```
78+
79+
## Application State
80+
81+
```python
82+
class RateLimiter:
83+
def __init__(self):
84+
self.rate_limited = False
85+
86+
def get_interval(self):
87+
return 10 if self.rate_limited else 1
88+
89+
rate_limiter = RateLimiter()
90+
91+
@backoff.on_predicate(
92+
backoff.constant,
93+
interval=lambda: rate_limiter.get_interval()
94+
)
95+
def adaptive_poll():
96+
return check_resource()
97+
```

0 commit comments

Comments
 (0)