Skip to content

Commit 716dc83

Browse files
committed
docs: adds snippets extension to the project
1 parent 4562297 commit 716dc83

File tree

104 files changed

+3826
-1136
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+3826
-1136
lines changed

.github/workflows/test-docs.yml

Lines changed: 67 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -3,75 +3,90 @@ name: Test Documentation
33
on:
44
push:
55
paths:
6-
- 'docs/src/snippets/**/*.py'
7-
- 'docs/src/**/*.md'
6+
- 'docs/snippets/**/*.py'
7+
- 'docs/markdown/**/*.md'
8+
- 'tests/unit/docs/**/*.py'
9+
- 'tests/integration/docs/**/*.py'
810
- '.github/workflows/test-docs.yml'
911
pull_request:
1012
paths:
11-
- 'docs/src/snippets/**/*.py'
12-
- 'docs/src/**/*.md'
13+
- 'docs/snippets/**/*.py'
14+
- 'docs/markdown/**/*.md'
15+
- 'tests/unit/docs/**/*.py'
16+
- 'tests/integration/docs/**/*.py'
1317
- '.github/workflows/test-docs.yml'
1418

19+
env:
20+
UV_LINK_MODE: "copy"
21+
UV_PYTHON_VERSION: "3.12"
22+
1523
jobs:
16-
validate-snippets:
17-
name: Validate Code Snippets
24+
unit-tests:
25+
name: Documentation Unit Tests
1826
runs-on: ubuntu-latest
1927
steps:
2028
- uses: actions/checkout@v5
2129

22-
- name: Setup Python
23-
uses: actions/setup-python@v5
30+
- name: Setup just command
31+
uses: extractions/setup-just@v3
2432
with:
25-
python-version: '3.12'
33+
just-version: '1.43.0'
34+
35+
- name: Install uv command
36+
uses: astral-sh/setup-uv@v7
2637

2738
- name: Install dependencies
28-
run: |
29-
pip install -e ".[dev]"
39+
run: just init $UV_PYTHON_VERSION
3040

31-
- name: Check Python syntax
32-
run: |
33-
echo "Checking Python syntax in docs/src/snippets/"
34-
python -m py_compile docs/src/snippets/**/*.py
35-
echo "All snippets have valid Python syntax"
41+
- name: Run snippet syntax tests
42+
run: uv run pytest tests/unit/docs/test_snippets_syntax.py -v
3643

37-
- name: Run snippet imports
38-
run: |
39-
echo "Verifying all snippets can be imported"
40-
python << 'EOF'
41-
import importlib.util
42-
import sys
43-
from pathlib import Path
44-
45-
snippets_dir = Path("docs/src/snippets")
46-
errors = []
47-
48-
for py_file in snippets_dir.rglob("*.py"):
49-
if py_file.name == "__init__.py":
50-
continue
51-
52-
module_name = py_file.stem
53-
spec = importlib.util.spec_from_file_location(module_name, py_file)
54-
55-
if spec and spec.loader:
56-
try:
57-
module = importlib.util.module_from_spec(spec)
58-
# Don't execute, just verify it can be loaded
59-
print(f"✓ {py_file.relative_to(snippets_dir)}")
60-
except Exception as e:
61-
errors.append(f"✗ {py_file.relative_to(snippets_dir)}: {e}")
62-
63-
if errors:
64-
print("\nErrors found:")
65-
for error in errors:
66-
print(error)
67-
sys.exit(1)
68-
else:
69-
print(f"\nAll {len(list(snippets_dir.rglob('*.py')))} snippets validated successfully")
70-
EOF
44+
- name: Run snippet marker tests
45+
run: uv run pytest tests/unit/docs/test_snippets_markers.py -v
46+
47+
- name: Run snippet import tests
48+
run: uv run pytest tests/unit/docs/test_snippets_imports.py -v
49+
50+
- name: Run all unit tests
51+
run: uv run pytest tests/unit/docs/ -v
52+
53+
integration-tests:
54+
name: Documentation Integration Tests
55+
runs-on: ubuntu-latest
56+
needs: unit-tests
57+
services:
58+
pubsub:
59+
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
60+
ports:
61+
- 8085:8085
62+
options: >-
63+
--health-cmd "curl -f http://localhost:8085 || exit 1"
64+
--health-interval 10s
65+
--health-timeout 5s
66+
--health-retries 5
67+
env:
68+
PUBSUB_EMULATOR_HOST: localhost:8085
69+
steps:
70+
- uses: actions/checkout@v5
71+
72+
- name: Setup just command
73+
uses: extractions/setup-just@v3
74+
with:
75+
just-version: '1.43.0'
76+
77+
- name: Install uv command
78+
uses: astral-sh/setup-uv@v7
79+
80+
- name: Install dependencies
81+
run: just init $UV_PYTHON_VERSION
82+
83+
- name: Run integration tests
84+
run: uv run pytest tests/integration/docs/ -v --timeout=60
7185

7286
build-docs:
7387
name: Build Documentation
7488
runs-on: ubuntu-latest
89+
needs: unit-tests
7590
steps:
7691
- uses: actions/checkout@v5
7792

@@ -87,49 +102,5 @@ jobs:
87102
run: |
88103
cd docs
89104
zensical build --clean
90-
echo "Documentation built successfully"
91-
92-
- name: Check for broken internal links
93-
run: |
94-
echo "Checking for broken links in documentation"
95-
python << 'EOF'
96-
import re
97-
from pathlib import Path
98-
99-
docs_dir = Path("docs/src")
100-
errors = []
101-
102-
# Pattern to match markdown links
103-
link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)')
104-
105-
for md_file in docs_dir.rglob("*.md"):
106-
content = md_file.read_text()
107-
108-
for match in link_pattern.finditer(content):
109-
link_text, link_url = match.groups()
110-
111-
# Skip external links
112-
if link_url.startswith(('http://', 'https://', '#', 'mailto:')):
113-
continue
114-
115-
# Resolve relative path
116-
if link_url.startswith('../'):
117-
target = (md_file.parent / link_url).resolve()
118-
else:
119-
target = (md_file.parent / link_url).resolve()
120-
121-
# Remove anchor if present
122-
target_path = str(target).split('#')[0]
123-
124-
if not Path(target_path).exists():
125-
errors.append(f"{md_file.relative_to(docs_dir)}: broken link '{link_url}'")
126-
127-
if errors:
128-
print("Broken links found:")
129-
for error in errors:
130-
print(f" ✗ {error}")
131-
# Warning only, don't fail the build
132-
print(f"\nFound {len(errors)} broken links (warning only)")
133-
else:
134-
print("No broken links found")
135-
EOF
105+
echo "Documentation built successfully with snippet inclusion"
106+
echo "All snippet references validated"

docs/markdown/tutorial/user-guide/cli.md

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,7 @@ The primary command is `run`, which takes one required argument: the path to you
3838
**Example application (`my_project/main.py`):**
3939

4040
```python
41-
from fastpubsub import FastPubSub, PubSubBroker
42-
43-
broker = PubSubBroker("your-project-id")
44-
app = FastPubSub(broker)
45-
46-
@broker.subscriber("process-orders", topic_name="orders", subscription_name="orders-sub")
47-
async def handle_orders(message): ...
48-
49-
@broker.subscriber("send-notifications", topic_name="notifications", subscription_name="notifications-sub")
50-
async def handle_notifications(message): ...
41+
--8<-- "basic_usage/e8_01_cli_example.py:cli_example_full"
5142
```
5243

5344
**Run with default settings:**

docs/markdown/tutorial/user-guide/deployment.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,7 @@ sudo systemctl status fastpubsub
219219
Never hardcode production values:
220220

221221
```python
222-
import os
223-
from fastpubsub import FastPubSub, PubSubBroker
224-
225-
PROJECT_ID = os.environ.get("GCP_PROJECT_ID")
226-
if not PROJECT_ID:
227-
raise RuntimeError("GCP_PROJECT_ID environment variable not set.")
228-
229-
broker = PubSubBroker(project_id=PROJECT_ID)
230-
app = FastPubSub(broker)
222+
--8<-- "basic_usage/e8_02_deployment_config.py:deployment_config_full"
231223
```
232224

233225
In Kubernetes, inject via `ConfigMaps` or `Secrets`.

docs/markdown/tutorial/user-guide/first-steps.md

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ FastPubSub has two main classes that form the backbone of every application:
2828
All Pub/Sub configuration attaches to the broker. The `FastPubSub` object takes a `PubSubBroker` instance as an argument. This separation lets you use all FastAPI features (middlewares, lifespan) with the application while integrating with the broker.
2929

3030
```python
31-
from fastpubsub import FastPubSub, PubSubBroker
32-
33-
broker = PubSubBroker(project_id="your-project-id")
34-
app = FastPubSub(broker)
31+
--8<-- "basic_usage/e0_01_first_steps.py:broker_app"
3532
```
3633

3734
---
@@ -41,36 +38,7 @@ app = FastPubSub(broker)
4138
Create a file named `basic.py`:
4239

4340
```python
44-
from pydantic import BaseModel, Field
45-
from fastpubsub import FastPubSub, PubSubBroker, Message
46-
from fastpubsub.logger import logger
47-
48-
49-
class Address(BaseModel):
50-
street: str = Field(..., examples=["5th Avenue"])
51-
number: str = Field(..., examples=["1548"])
52-
53-
54-
broker = PubSubBroker(project_id="your-project-id")
55-
app = FastPubSub(broker)
56-
57-
58-
@app.post("/addresses/")
59-
async def create_address(address: Address):
60-
logger.info(f"Address received: {address}")
61-
await broker.publish(topic_name="address-events", data=address)
62-
return {"message": "Address published"}
63-
64-
65-
@broker.subscriber(
66-
alias="address-handler",
67-
topic_name="address-events",
68-
subscription_name="address-events-subscription",
69-
)
70-
async def handle_message(message: Message):
71-
logger.info(f"The message {message.id} arrived.")
72-
address = Address.model_validate_json(message.data)
73-
logger.info(f"Address: {address}")
41+
--8<-- "basic_usage/e0_01_first_steps.py:first_steps_full"
7442
```
7543

7644
This application:
@@ -163,10 +131,6 @@ Notice how the logs include context like `message_id`, `topic_name`, and the han
163131

164132
### The Broker
165133

166-
```python
167-
broker = PubSubBroker(project_id="your-project-id")
168-
```
169-
170134
The broker manages all Pub/Sub connections. It handles:
171135

172136
- Creating topics and subscriptions (when `autocreate=True`)
@@ -175,10 +139,6 @@ The broker manages all Pub/Sub connections. It handles:
175139

176140
### The Application
177141

178-
```python
179-
app = FastPubSub(broker)
180-
```
181-
182142
The application is a FastAPI instance with Pub/Sub integration. You can use all FastAPI features like:
183143

184144
- Path operations (`@app.get()`, `@app.post()`)
@@ -189,13 +149,7 @@ The application is a FastAPI instance with Pub/Sub integration. You can use all
189149
### The Subscriber
190150

191151
```python
192-
@broker.subscriber(
193-
alias="address-handler",
194-
topic_name="address-events",
195-
subscription_name="address-events-subscription",
196-
)
197-
async def handle_message(message: Message):
198-
...
152+
--8<-- "basic_usage/e0_01_first_steps.py:subscriber"
199153
```
200154

201155
The `@broker.subscriber` decorator registers an async function as a message handler. Key parameters:
@@ -211,7 +165,7 @@ By default, `autocreate=True`, so FastPubSub creates the topic and subscription
211165
### Publishing
212166

213167
```python
214-
await broker.publish(topic_name="address-events", data=address)
168+
--8<-- "basic_usage/e0_01_first_steps.py:rest_endpoint"
215169
```
216170

217171
The broker's `publish` method sends messages to a topic. It automatically serializes:

docs/markdown/tutorial/user-guide/lifecycle.md

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,7 @@ Sometimes, you receive a message that you cannot process, but you don't want it
5454

5555

5656
```python
57-
from fastpubsub.exceptions import Drop
58-
59-
@broker.subscriber(...)
60-
async def handle_events(message: Message):
61-
event_attributes = message.attributes
62-
if event_attributes.get("schema_version") == "v1":
63-
# We no longer support v1 events
64-
raise Drop("Schema version v1 is deprecated.")
65-
66-
# Process v2+ events...
57+
--8<-- "basic_usage/e7_01_lifecycle_drop.py:drop_handler"
6758
```
6859

6960

@@ -76,18 +67,7 @@ For temporary, recoverable errors (e.g., a database is temporarily unavailable,
7667

7768

7869
```python
79-
from fastpubsub.exceptions import Retry
80-
import httpx
81-
82-
@broker.subscriber(...)
83-
async def handle_order(message: Message):
84-
order_id = json.loads(message.data)["order_id"]
85-
try:
86-
async with httpx.AsyncClient() as client:
87-
await client.post(f"https://downstream.service/process/{order_id}")
88-
except httpx.TimeoutException:
89-
# Service is slow, retry later
90-
raise Retry("Downstream service timed out.")
70+
--8<-- "basic_usage/e7_02_lifecycle_retry.py:retry_handler"
9171
```
9272

9373
!!! tip "Exponential Backoff"
@@ -103,12 +83,7 @@ Any exception that is not `Drop` or `Retry` is considered an unhandled unexpecte
10383

10484

10585
```python
106-
@broker.subscriber(...)
107-
async def handle_event(message: Message):
108-
# If this raises ValueError, KeyError, etc.
109-
# the message is nacked and redelivered
110-
data = json.loads(message.data)
111-
await process(data)
86+
--8<-- "basic_usage/e7_03_lifecycle_unhandled.py:unhandled_handler"
11287
```
11388

11489
---

0 commit comments

Comments
 (0)