@girardmedia/bootspring 3.3.2 → 3.4.0
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.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: python-patterns
|
|
3
|
+
description: Modern Python patterns including type hints, dataclasses, async/await, context managers, decorators, and pathlib.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when writing Python 3.10+ code that needs to be maintainable,
|
|
11
|
+
type-safe, and idiomatic. Use this skill when building CLI tools, data pipelines,
|
|
12
|
+
API clients, or any Python project that benefits from modern language features.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### Type Hints and Generics
|
|
17
|
+
|
|
18
|
+
Use `from __future__ import annotations` at the top of every module. Prefer built-in
|
|
19
|
+
generic types (`list[str]`, `dict[str, int]`) over `typing.List`, `typing.Dict`.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
from typing import TypeVar, Protocol
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
class Comparable(Protocol):
|
|
28
|
+
def __lt__(self, other: Comparable) -> bool: ...
|
|
29
|
+
|
|
30
|
+
def top_n(items: list[T], n: int, key: Callable[[T], Comparable]) -> list[T]:
|
|
31
|
+
return sorted(items, key=key, reverse=True)[:n]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Dataclasses and Slots
|
|
35
|
+
|
|
36
|
+
Use `@dataclass(frozen=True, slots=True)` for value objects. Use `field(default_factory=...)`
|
|
37
|
+
for mutable defaults. Never use mutable default arguments.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from dataclasses import dataclass, field
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True, slots=True)
|
|
43
|
+
class Config:
|
|
44
|
+
host: str
|
|
45
|
+
port: int = 8080
|
|
46
|
+
tags: list[str] = field(default_factory=list)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Async/Await
|
|
50
|
+
|
|
51
|
+
Use `asyncio.TaskGroup` (3.11+) instead of `asyncio.gather` for structured concurrency.
|
|
52
|
+
Always set timeouts on network calls. Use `async for` with async iterators.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
async def fetch_all(urls: list[str]) -> list[Response]:
|
|
56
|
+
async with asyncio.TaskGroup() as tg:
|
|
57
|
+
tasks = [tg.create_task(fetch(url)) for url in urls]
|
|
58
|
+
return [t.result() for t in tasks]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Context Managers
|
|
62
|
+
|
|
63
|
+
Use `@contextmanager` for resource cleanup. Pair every acquire with a release in a
|
|
64
|
+
try/finally block. Use `AsyncExitStack` when managing dynamic numbers of resources.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from contextlib import contextmanager
|
|
68
|
+
|
|
69
|
+
@contextmanager
|
|
70
|
+
def temp_directory(prefix: str = "tmp"):
|
|
71
|
+
path = Path(tempfile.mkdtemp(prefix=prefix))
|
|
72
|
+
try:
|
|
73
|
+
yield path
|
|
74
|
+
finally:
|
|
75
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Decorators with Proper Signatures
|
|
79
|
+
|
|
80
|
+
Always use `@functools.wraps` to preserve the wrapped function's metadata.
|
|
81
|
+
Use `ParamSpec` for type-safe decorators that forward arguments.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from typing import ParamSpec, TypeVar
|
|
85
|
+
import functools
|
|
86
|
+
|
|
87
|
+
P = ParamSpec("P")
|
|
88
|
+
R = TypeVar("R")
|
|
89
|
+
|
|
90
|
+
def retry(max_attempts: int = 3):
|
|
91
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
92
|
+
@functools.wraps(func)
|
|
93
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
94
|
+
for attempt in range(max_attempts):
|
|
95
|
+
try:
|
|
96
|
+
return func(*args, **kwargs)
|
|
97
|
+
except Exception:
|
|
98
|
+
if attempt == max_attempts - 1:
|
|
99
|
+
raise
|
|
100
|
+
raise RuntimeError("unreachable")
|
|
101
|
+
return wrapper
|
|
102
|
+
return decorator
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Pathlib Over os.path
|
|
106
|
+
|
|
107
|
+
Use `Path` for all filesystem operations. Chain `/` for path construction.
|
|
108
|
+
Use `.read_text()` / `.write_text()` instead of `open()` for simple reads/writes.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from pathlib import Path
|
|
112
|
+
|
|
113
|
+
config_dir = Path.home() / ".config" / "myapp"
|
|
114
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
config = config_dir / "settings.toml"
|
|
116
|
+
content = config.read_text(encoding="utf-8")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Examples
|
|
120
|
+
|
|
121
|
+
**Pattern: Structured enum with behavior**
|
|
122
|
+
```python
|
|
123
|
+
from enum import Enum, auto
|
|
124
|
+
|
|
125
|
+
class Status(Enum):
|
|
126
|
+
PENDING = auto()
|
|
127
|
+
RUNNING = auto()
|
|
128
|
+
DONE = auto()
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def is_terminal(self) -> bool:
|
|
132
|
+
return self in (Status.DONE,)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Pattern: Named tuple for lightweight records**
|
|
136
|
+
```python
|
|
137
|
+
from typing import NamedTuple
|
|
138
|
+
|
|
139
|
+
class Point(NamedTuple):
|
|
140
|
+
x: float
|
|
141
|
+
y: float
|
|
142
|
+
|
|
143
|
+
def distance_to(self, other: Point) -> float:
|
|
144
|
+
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Checklist
|
|
148
|
+
|
|
149
|
+
- [ ] `from __future__ import annotations` at top of every module
|
|
150
|
+
- [ ] All function signatures have type hints, including return types
|
|
151
|
+
- [ ] Dataclasses use `frozen=True` and `slots=True` where possible
|
|
152
|
+
- [ ] No mutable default arguments (use `field(default_factory=...)`)
|
|
153
|
+
- [ ] `@functools.wraps` on every decorator
|
|
154
|
+
- [ ] `Path` instead of `os.path` for filesystem operations
|
|
155
|
+
- [ ] `asyncio.TaskGroup` instead of bare `gather` for structured concurrency
|
|
156
|
+
- [ ] Context managers for any resource that needs cleanup
|
|
157
|
+
- [ ] `match` statements (3.10+) instead of long if/elif chains where appropriate
|
|
158
|
+
- [ ] `__all__` defined in public-facing modules
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: python-testing
|
|
3
|
+
description: Python testing with pytest including fixtures, parametrize, mocking, coverage, and property-based testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python Testing
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply this skill when writing or improving tests for Python code. Use pytest as the
|
|
11
|
+
default test runner. Reach for property-based testing (Hypothesis) when testing pure
|
|
12
|
+
functions with wide input domains. Use mocking sparingly and prefer dependency injection.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### Pytest Fixtures
|
|
17
|
+
|
|
18
|
+
Fixtures replace setup/teardown. Use `yield` fixtures for cleanup. Scope fixtures
|
|
19
|
+
(`session`, `module`, `function`) to control lifecycle. Keep fixtures close to the
|
|
20
|
+
tests that use them; use `conftest.py` for shared fixtures.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import pytest
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def db_conn():
|
|
27
|
+
conn = create_test_connection()
|
|
28
|
+
yield conn
|
|
29
|
+
conn.rollback()
|
|
30
|
+
conn.close()
|
|
31
|
+
|
|
32
|
+
@pytest.fixture(scope="session")
|
|
33
|
+
def app_config():
|
|
34
|
+
return load_config("test")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Parametrize
|
|
38
|
+
|
|
39
|
+
Use `@pytest.mark.parametrize` for data-driven tests. Each tuple is a distinct test
|
|
40
|
+
case with its own pass/fail. Use `pytest.param(..., id="name")` for readable IDs.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
@pytest.mark.parametrize("input_val,expected", [
|
|
44
|
+
pytest.param("hello", "HELLO", id="lowercase"),
|
|
45
|
+
pytest.param("Hello World", "HELLO WORLD", id="mixed-case"),
|
|
46
|
+
pytest.param("", "", id="empty-string"),
|
|
47
|
+
pytest.param("123", "123", id="digits-unchanged"),
|
|
48
|
+
])
|
|
49
|
+
def test_uppercase(input_val: str, expected: str):
|
|
50
|
+
assert input_val.upper() == expected
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Mocking
|
|
54
|
+
|
|
55
|
+
Use `unittest.mock.patch` to replace external dependencies. Prefer patching where
|
|
56
|
+
the name is used, not where it is defined. Use `spec=True` to catch attribute errors.
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from unittest.mock import patch, MagicMock
|
|
60
|
+
|
|
61
|
+
def test_send_email():
|
|
62
|
+
with patch("myapp.notifications.smtp_client", spec=True) as mock_smtp:
|
|
63
|
+
mock_smtp.send.return_value = {"status": "sent"}
|
|
64
|
+
result = send_notification("user@example.com", "Hello")
|
|
65
|
+
mock_smtp.send.assert_called_once()
|
|
66
|
+
assert result["status"] == "sent"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Async Testing
|
|
70
|
+
|
|
71
|
+
Use `pytest-asyncio` with `@pytest.mark.asyncio` for async tests. Set
|
|
72
|
+
`asyncio_mode = "auto"` in `pyproject.toml` to avoid marking every test.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
@pytest.mark.asyncio
|
|
76
|
+
async def test_fetch_user(mock_api):
|
|
77
|
+
user = await fetch_user(user_id=42)
|
|
78
|
+
assert user.name == "Alice"
|
|
79
|
+
assert user.active is True
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Property-Based Testing with Hypothesis
|
|
83
|
+
|
|
84
|
+
Use Hypothesis for pure functions. Define strategies that describe valid inputs.
|
|
85
|
+
Hypothesis will find edge cases you would never write by hand.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from hypothesis import given, strategies as st
|
|
89
|
+
|
|
90
|
+
@given(st.lists(st.integers()))
|
|
91
|
+
def test_sort_is_idempotent(xs: list[int]):
|
|
92
|
+
sorted_once = sorted(xs)
|
|
93
|
+
sorted_twice = sorted(sorted_once)
|
|
94
|
+
assert sorted_once == sorted_twice
|
|
95
|
+
|
|
96
|
+
@given(st.text(min_size=1))
|
|
97
|
+
def test_round_trip_encode_decode(s: str):
|
|
98
|
+
assert decode(encode(s)) == s
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Coverage
|
|
102
|
+
|
|
103
|
+
Run with `pytest --cov=myapp --cov-report=term-missing --cov-fail-under=80`.
|
|
104
|
+
Configure in `pyproject.toml`:
|
|
105
|
+
|
|
106
|
+
```toml
|
|
107
|
+
[tool.coverage.run]
|
|
108
|
+
source = ["myapp"]
|
|
109
|
+
omit = ["*/tests/*", "*/migrations/*"]
|
|
110
|
+
|
|
111
|
+
[tool.coverage.report]
|
|
112
|
+
fail_under = 80
|
|
113
|
+
show_missing = true
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Markers and Filtering
|
|
117
|
+
|
|
118
|
+
Use custom markers to categorize tests. Run subsets with `-m`.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
@pytest.mark.slow
|
|
122
|
+
def test_full_pipeline():
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
@pytest.mark.integration
|
|
126
|
+
def test_database_roundtrip(db_conn):
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pytest -m "not slow" # skip slow tests
|
|
132
|
+
pytest -m integration # run only integration tests
|
|
133
|
+
pytest -k "test_parse" # run tests matching name pattern
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Examples
|
|
137
|
+
|
|
138
|
+
**Pattern: Factory fixture for flexible test data**
|
|
139
|
+
```python
|
|
140
|
+
@pytest.fixture
|
|
141
|
+
def make_user():
|
|
142
|
+
def _make(name: str = "Alice", role: str = "user") -> User:
|
|
143
|
+
return User(id=uuid4(), name=name, role=role)
|
|
144
|
+
return _make
|
|
145
|
+
|
|
146
|
+
def test_admin_permissions(make_user):
|
|
147
|
+
admin = make_user(role="admin")
|
|
148
|
+
assert admin.can_delete_users() is True
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Pattern: tmp_path for file tests (built-in fixture)**
|
|
152
|
+
```python
|
|
153
|
+
def test_write_config(tmp_path):
|
|
154
|
+
config_file = tmp_path / "config.json"
|
|
155
|
+
write_config(config_file, {"debug": True})
|
|
156
|
+
assert config_file.exists()
|
|
157
|
+
data = json.loads(config_file.read_text())
|
|
158
|
+
assert data["debug"] is True
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Checklist
|
|
162
|
+
|
|
163
|
+
- [ ] Every test function starts with `test_` and has a descriptive name
|
|
164
|
+
- [ ] Fixtures use `yield` for cleanup, not `try/finally` in test bodies
|
|
165
|
+
- [ ] `@pytest.mark.parametrize` for any test with more than 2 similar cases
|
|
166
|
+
- [ ] Mocks use `spec=True` to catch incorrect attribute access
|
|
167
|
+
- [ ] Patch targets where the name is imported, not where it is defined
|
|
168
|
+
- [ ] Async tests use `@pytest.mark.asyncio` or `asyncio_mode = "auto"`
|
|
169
|
+
- [ ] Coverage threshold set in `pyproject.toml` (`fail_under = 80`)
|
|
170
|
+
- [ ] Hypothesis tests for pure functions with wide input domains
|
|
171
|
+
- [ ] Slow/integration tests marked so CI can run them separately
|
|
172
|
+
- [ ] No test depends on execution order (use `pytest-randomly` to verify)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: queue-patterns
|
|
3
|
+
description: Queue patterns for FIFO processing, priority queues, dead letter queues, delayed jobs, and exactly-once processing with BullMQ.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Queue Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use job queues when you need to defer work, process tasks asynchronously, handle spikes in workload, or ensure reliable execution of operations that may fail. Queues decouple producers from consumers, enabling retry logic, priority ordering, rate-controlled processing, and distributed workloads. Apply these patterns for email sending, image processing, webhook delivery, report generation, and any task that should not block the request-response cycle.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### BullMQ Queue Setup
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/queues/index.ts
|
|
17
|
+
import { Queue, Worker, QueueEvents } from 'bullmq';
|
|
18
|
+
import Redis from 'ioredis';
|
|
19
|
+
|
|
20
|
+
const connection = new Redis(process.env.REDIS_URL!, { maxRetriesPerRequest: null });
|
|
21
|
+
|
|
22
|
+
// Define typed job data
|
|
23
|
+
interface EmailJobData {
|
|
24
|
+
to: string;
|
|
25
|
+
subject: string;
|
|
26
|
+
template: string;
|
|
27
|
+
variables: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ImageJobData {
|
|
31
|
+
sourceUrl: string;
|
|
32
|
+
outputPath: string;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
format: 'webp' | 'avif' | 'jpeg';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create queues
|
|
39
|
+
export const emailQueue = new Queue<EmailJobData>('email', {
|
|
40
|
+
connection,
|
|
41
|
+
defaultJobOptions: {
|
|
42
|
+
attempts: 3,
|
|
43
|
+
backoff: { type: 'exponential', delay: 5000 },
|
|
44
|
+
removeOnComplete: { age: 86400, count: 1000 },
|
|
45
|
+
removeOnFail: { age: 604800 },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const imageQueue = new Queue<ImageJobData>('image-processing', {
|
|
50
|
+
connection,
|
|
51
|
+
defaultJobOptions: {
|
|
52
|
+
attempts: 2,
|
|
53
|
+
backoff: { type: 'fixed', delay: 10000 },
|
|
54
|
+
timeout: 120_000,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Worker Definition
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// src/workers/email.worker.ts
|
|
63
|
+
import { Worker, Job } from 'bullmq';
|
|
64
|
+
import { sendEmail } from '../services/email';
|
|
65
|
+
|
|
66
|
+
const emailWorker = new Worker<EmailJobData>(
|
|
67
|
+
'email',
|
|
68
|
+
async (job: Job<EmailJobData>) => {
|
|
69
|
+
const { to, subject, template, variables } = job.data;
|
|
70
|
+
|
|
71
|
+
await job.updateProgress(10);
|
|
72
|
+
|
|
73
|
+
const html = await renderTemplate(template, variables);
|
|
74
|
+
await job.updateProgress(50);
|
|
75
|
+
|
|
76
|
+
const result = await sendEmail({ to, subject, html });
|
|
77
|
+
await job.updateProgress(100);
|
|
78
|
+
|
|
79
|
+
return { messageId: result.messageId, sentAt: new Date().toISOString() };
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
connection,
|
|
83
|
+
concurrency: 5, // process 5 jobs simultaneously
|
|
84
|
+
limiter: { max: 50, duration: 60_000 }, // max 50 per minute
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
emailWorker.on('completed', (job) => {
|
|
89
|
+
console.log(`Email sent: ${job.id} to ${job.data.to}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
emailWorker.on('failed', (job, err) => {
|
|
93
|
+
console.error(`Email failed: ${job?.id}`, err.message);
|
|
94
|
+
if (job && job.attemptsMade >= (job.opts.attempts ?? 3)) {
|
|
95
|
+
// Move to dead letter queue after all retries exhausted
|
|
96
|
+
deadLetterQueue.add('email-failed', { originalJob: job.data, error: err.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Priority Queue
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// src/queues/priority.ts
|
|
105
|
+
export async function addJobWithPriority(data: EmailJobData, priority: 'high' | 'normal' | 'low') {
|
|
106
|
+
const priorityMap = { high: 1, normal: 5, low: 10 };
|
|
107
|
+
|
|
108
|
+
await emailQueue.add('send-email', data, {
|
|
109
|
+
priority: priorityMap[priority],
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// High-priority transactional emails
|
|
114
|
+
await addJobWithPriority({
|
|
115
|
+
to: 'user@example.com',
|
|
116
|
+
subject: 'Password Reset',
|
|
117
|
+
template: 'password-reset',
|
|
118
|
+
variables: { resetLink: 'https://...' },
|
|
119
|
+
}, 'high');
|
|
120
|
+
|
|
121
|
+
// Low-priority marketing emails
|
|
122
|
+
await addJobWithPriority({
|
|
123
|
+
to: 'user@example.com',
|
|
124
|
+
subject: 'Weekly Digest',
|
|
125
|
+
template: 'weekly-digest',
|
|
126
|
+
variables: { userName: 'Alice' },
|
|
127
|
+
}, 'low');
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Delayed Jobs
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
// src/queues/scheduled.ts
|
|
134
|
+
// Send email 24 hours after signup
|
|
135
|
+
export async function scheduleWelcomeEmail(userId: string, email: string) {
|
|
136
|
+
await emailQueue.add('welcome-email', {
|
|
137
|
+
to: email,
|
|
138
|
+
subject: 'Getting started with MyApp',
|
|
139
|
+
template: 'welcome',
|
|
140
|
+
variables: { userId },
|
|
141
|
+
}, {
|
|
142
|
+
delay: 24 * 60 * 60 * 1000, // 24 hours
|
|
143
|
+
jobId: `welcome-${userId}`, // prevent duplicates
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Repeating jobs (cron-like)
|
|
148
|
+
export async function setupRecurringJobs() {
|
|
149
|
+
await emailQueue.add('daily-digest', {
|
|
150
|
+
to: 'admin@example.com',
|
|
151
|
+
subject: 'Daily Report',
|
|
152
|
+
template: 'daily-report',
|
|
153
|
+
variables: {},
|
|
154
|
+
}, {
|
|
155
|
+
repeat: {
|
|
156
|
+
pattern: '0 9 * * *', // every day at 9 AM
|
|
157
|
+
tz: 'America/New_York',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Dead Letter Queue
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// src/queues/dead-letter.ts
|
|
167
|
+
export const deadLetterQueue = new Queue('dead-letter', {
|
|
168
|
+
connection,
|
|
169
|
+
defaultJobOptions: {
|
|
170
|
+
removeOnComplete: false, // keep for inspection
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Worker to handle dead letter inspection
|
|
175
|
+
const dlqWorker = new Worker(
|
|
176
|
+
'dead-letter',
|
|
177
|
+
async (job) => {
|
|
178
|
+
// Log to monitoring
|
|
179
|
+
await logToMonitoring({
|
|
180
|
+
type: 'dead-letter',
|
|
181
|
+
originalQueue: job.name,
|
|
182
|
+
data: job.data,
|
|
183
|
+
timestamp: new Date().toISOString(),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Alert on critical failures
|
|
187
|
+
if (job.data.originalJob?.template === 'password-reset') {
|
|
188
|
+
await alertOncall('Critical email delivery failure', job.data);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{ connection, concurrency: 1 }
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Admin API to retry dead-lettered jobs
|
|
195
|
+
export async function retryDeadLetter(jobId: string) {
|
|
196
|
+
const job = await deadLetterQueue.getJob(jobId);
|
|
197
|
+
if (!job) throw new Error(`Job ${jobId} not found`);
|
|
198
|
+
|
|
199
|
+
const { originalJob } = job.data;
|
|
200
|
+
await emailQueue.add('retry', originalJob, { attempts: 1 });
|
|
201
|
+
await job.remove();
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Exactly-Once Processing Pattern
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
// src/workers/idempotent.ts
|
|
209
|
+
import { Job } from 'bullmq';
|
|
210
|
+
|
|
211
|
+
async function processIdempotent(job: Job) {
|
|
212
|
+
const idempotencyKey = `processed:${job.id}`;
|
|
213
|
+
|
|
214
|
+
// Check if already processed
|
|
215
|
+
const alreadyProcessed = await redis.get(idempotencyKey);
|
|
216
|
+
if (alreadyProcessed) {
|
|
217
|
+
console.log(`Job ${job.id} already processed, skipping`);
|
|
218
|
+
return JSON.parse(alreadyProcessed);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Process the job
|
|
222
|
+
const result = await doWork(job.data);
|
|
223
|
+
|
|
224
|
+
// Mark as processed with TTL
|
|
225
|
+
await redis.set(idempotencyKey, JSON.stringify(result), 'EX', 86400);
|
|
226
|
+
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Queue Monitoring Dashboard
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// src/routes/admin/queues.ts
|
|
235
|
+
import { Queue } from 'bullmq';
|
|
236
|
+
|
|
237
|
+
export async function getQueueStats(queue: Queue) {
|
|
238
|
+
const [waiting, active, completed, failed, delayed] = await Promise.all([
|
|
239
|
+
queue.getWaitingCount(),
|
|
240
|
+
queue.getActiveCount(),
|
|
241
|
+
queue.getCompletedCount(),
|
|
242
|
+
queue.getFailedCount(),
|
|
243
|
+
queue.getDelayedCount(),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
name: queue.name,
|
|
248
|
+
counts: { waiting, active, completed, failed, delayed },
|
|
249
|
+
isPaused: await queue.isPaused(),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
app.get('/admin/queues', async (_req, res) => {
|
|
254
|
+
const stats = await Promise.all([
|
|
255
|
+
getQueueStats(emailQueue),
|
|
256
|
+
getQueueStats(imageQueue),
|
|
257
|
+
getQueueStats(deadLetterQueue),
|
|
258
|
+
]);
|
|
259
|
+
res.json({ queues: stats });
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Pause/resume for maintenance
|
|
263
|
+
app.post('/admin/queues/:name/pause', async (req, res) => {
|
|
264
|
+
const queue = getQueueByName(req.params.name);
|
|
265
|
+
await queue.pause();
|
|
266
|
+
res.json({ paused: true });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
app.post('/admin/queues/:name/resume', async (req, res) => {
|
|
270
|
+
const queue = getQueueByName(req.params.name);
|
|
271
|
+
await queue.resume();
|
|
272
|
+
res.json({ paused: false });
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Examples
|
|
277
|
+
|
|
278
|
+
| Pattern | Use Case | Retry Strategy |
|
|
279
|
+
|---------|----------|----------------|
|
|
280
|
+
| FIFO | Order processing | Exponential backoff, 3 attempts |
|
|
281
|
+
| Priority | Transactional vs marketing email | Priority 1/5/10, same retry |
|
|
282
|
+
| Delayed | Welcome email after 24h | Single attempt, no retry |
|
|
283
|
+
| Repeating | Daily report generation | Cron pattern, skip on failure |
|
|
284
|
+
| Dead letter | Failed after all retries | Manual inspection and retry |
|
|
285
|
+
| Batch | Bulk CSV import | Chunk into sub-jobs, parallel |
|
|
286
|
+
|
|
287
|
+
## Checklist
|
|
288
|
+
- [ ] Queues use `maxRetriesPerRequest: null` in Redis connection for BullMQ
|
|
289
|
+
- [ ] Default job options set for attempts, backoff, and TTL
|
|
290
|
+
- [ ] Workers have concurrency limits matching resource capacity
|
|
291
|
+
- [ ] Rate limiter configured for external API calls (email, SMS)
|
|
292
|
+
- [ ] Dead letter queue captures jobs that exhaust all retries
|
|
293
|
+
- [ ] Idempotency keys prevent duplicate processing on retry
|
|
294
|
+
- [ ] Queue monitoring endpoint exposes counts and pause state
|
|
295
|
+
- [ ] Delayed and repeating jobs use unique `jobId` to prevent duplicates
|