autoworkflow 3.1.5 → 3.5.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/.claude/commands/analyze.md +19 -0
- package/.claude/commands/audit.md +26 -0
- package/.claude/commands/build.md +39 -0
- package/.claude/commands/commit.md +25 -0
- package/.claude/commands/fix.md +23 -0
- package/.claude/commands/plan.md +18 -0
- package/.claude/commands/suggest.md +23 -0
- package/.claude/commands/verify.md +18 -0
- package/.claude/hooks/post-bash-router.sh +20 -0
- package/.claude/hooks/post-commit.sh +140 -0
- package/.claude/hooks/pre-edit.sh +129 -0
- package/.claude/hooks/session-check.sh +79 -0
- package/.claude/settings.json +40 -6
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/actix.md +337 -0
- package/.claude/skills/alembic.md +504 -0
- package/.claude/skills/angular.md +237 -0
- package/.claude/skills/api-design.md +187 -0
- package/.claude/skills/aspnet-core.md +377 -0
- package/.claude/skills/astro.md +245 -0
- package/.claude/skills/auth-clerk.md +327 -0
- package/.claude/skills/auth-firebase.md +367 -0
- package/.claude/skills/auth-nextauth.md +359 -0
- package/.claude/skills/auth-supabase.md +368 -0
- package/.claude/skills/axum.md +386 -0
- package/.claude/skills/blazor.md +456 -0
- package/.claude/skills/chi.md +348 -0
- package/.claude/skills/code-review.md +133 -0
- package/.claude/skills/csharp.md +296 -0
- package/.claude/skills/css-modules.md +325 -0
- package/.claude/skills/cypress.md +343 -0
- package/.claude/skills/debugging.md +133 -0
- package/.claude/skills/diesel.md +392 -0
- package/.claude/skills/django.md +301 -0
- package/.claude/skills/docker.md +319 -0
- package/.claude/skills/doctrine.md +473 -0
- package/.claude/skills/documentation.md +182 -0
- package/.claude/skills/dotnet.md +409 -0
- package/.claude/skills/drizzle.md +293 -0
- package/.claude/skills/echo.md +321 -0
- package/.claude/skills/eloquent.md +256 -0
- package/.claude/skills/emotion.md +426 -0
- package/.claude/skills/entity-framework.md +370 -0
- package/.claude/skills/express.md +316 -0
- package/.claude/skills/fastapi.md +329 -0
- package/.claude/skills/fastify.md +299 -0
- package/.claude/skills/fiber.md +315 -0
- package/.claude/skills/flask.md +322 -0
- package/.claude/skills/gin.md +342 -0
- package/.claude/skills/git.md +116 -0
- package/.claude/skills/github-actions.md +353 -0
- package/.claude/skills/go.md +377 -0
- package/.claude/skills/gorm.md +409 -0
- package/.claude/skills/graphql.md +478 -0
- package/.claude/skills/hibernate.md +379 -0
- package/.claude/skills/hono.md +306 -0
- package/.claude/skills/java.md +400 -0
- package/.claude/skills/jest.md +313 -0
- package/.claude/skills/jpa.md +282 -0
- package/.claude/skills/kotlin.md +347 -0
- package/.claude/skills/kubernetes.md +363 -0
- package/.claude/skills/laravel.md +414 -0
- package/.claude/skills/mcp-browser.md +320 -0
- package/.claude/skills/mcp-database.md +219 -0
- package/.claude/skills/mcp-fetch.md +241 -0
- package/.claude/skills/mcp-filesystem.md +204 -0
- package/.claude/skills/mcp-github.md +217 -0
- package/.claude/skills/mcp-memory.md +240 -0
- package/.claude/skills/mcp-search.md +218 -0
- package/.claude/skills/mcp-slack.md +262 -0
- package/.claude/skills/micronaut.md +388 -0
- package/.claude/skills/mongodb.md +319 -0
- package/.claude/skills/mongoose.md +355 -0
- package/.claude/skills/mysql.md +281 -0
- package/.claude/skills/nestjs.md +335 -0
- package/.claude/skills/nextjs-app-router.md +260 -0
- package/.claude/skills/nextjs-pages.md +172 -0
- package/.claude/skills/nuxt.md +202 -0
- package/.claude/skills/openapi.md +489 -0
- package/.claude/skills/performance.md +199 -0
- package/.claude/skills/php.md +398 -0
- package/.claude/skills/playwright.md +371 -0
- package/.claude/skills/postgresql.md +257 -0
- package/.claude/skills/prisma.md +293 -0
- package/.claude/skills/pydantic.md +304 -0
- package/.claude/skills/pytest.md +313 -0
- package/.claude/skills/python.md +272 -0
- package/.claude/skills/quarkus.md +377 -0
- package/.claude/skills/react.md +230 -0
- package/.claude/skills/redis.md +391 -0
- package/.claude/skills/refactoring.md +143 -0
- package/.claude/skills/remix.md +246 -0
- package/.claude/skills/rest-api.md +490 -0
- package/.claude/skills/rocket.md +366 -0
- package/.claude/skills/rust.md +341 -0
- package/.claude/skills/sass.md +380 -0
- package/.claude/skills/sea-orm.md +382 -0
- package/.claude/skills/security.md +167 -0
- package/.claude/skills/sequelize.md +395 -0
- package/.claude/skills/spring-boot.md +416 -0
- package/.claude/skills/sqlalchemy.md +269 -0
- package/.claude/skills/sqlx-rust.md +408 -0
- package/.claude/skills/state-jotai.md +346 -0
- package/.claude/skills/state-mobx.md +353 -0
- package/.claude/skills/state-pinia.md +431 -0
- package/.claude/skills/state-redux.md +337 -0
- package/.claude/skills/state-tanstack-query.md +434 -0
- package/.claude/skills/state-zustand.md +340 -0
- package/.claude/skills/styled-components.md +403 -0
- package/.claude/skills/svelte.md +238 -0
- package/.claude/skills/sveltekit.md +207 -0
- package/.claude/skills/symfony.md +437 -0
- package/.claude/skills/tailwind.md +279 -0
- package/.claude/skills/terraform.md +394 -0
- package/.claude/skills/testing-library.md +371 -0
- package/.claude/skills/trpc.md +426 -0
- package/.claude/skills/typeorm.md +368 -0
- package/.claude/skills/vitest.md +330 -0
- package/.claude/skills/vue.md +202 -0
- package/.claude/skills/warp.md +365 -0
- package/README.md +135 -52
- package/package.json +1 -1
- package/system/triggers.md +152 -11
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Pytest Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
tests/
|
|
6
|
+
├── conftest.py # Shared fixtures
|
|
7
|
+
├── unit/
|
|
8
|
+
│ ├── test_models.py
|
|
9
|
+
│ └── test_services.py
|
|
10
|
+
├── integration/
|
|
11
|
+
│ └── test_api.py
|
|
12
|
+
└── e2e/
|
|
13
|
+
└── test_flows.py
|
|
14
|
+
\`\`\`
|
|
15
|
+
|
|
16
|
+
## Fixtures
|
|
17
|
+
\`\`\`python
|
|
18
|
+
# conftest.py
|
|
19
|
+
import pytest
|
|
20
|
+
from sqlalchemy import create_engine
|
|
21
|
+
from sqlalchemy.orm import sessionmaker
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(scope="session")
|
|
24
|
+
def engine():
|
|
25
|
+
"""Create test database engine once per test session."""
|
|
26
|
+
engine = create_engine("sqlite:///:memory:")
|
|
27
|
+
Base.metadata.create_all(engine)
|
|
28
|
+
yield engine
|
|
29
|
+
engine.dispose()
|
|
30
|
+
|
|
31
|
+
@pytest.fixture(scope="function")
|
|
32
|
+
def db_session(engine):
|
|
33
|
+
"""Create a new session for each test."""
|
|
34
|
+
Session = sessionmaker(bind=engine)
|
|
35
|
+
session = Session()
|
|
36
|
+
yield session
|
|
37
|
+
session.rollback()
|
|
38
|
+
session.close()
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def user(db_session):
|
|
42
|
+
"""Create a test user."""
|
|
43
|
+
user = User(email="test@example.com", name="Test User")
|
|
44
|
+
db_session.add(user)
|
|
45
|
+
db_session.commit()
|
|
46
|
+
return user
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def client(app):
|
|
50
|
+
"""Flask test client."""
|
|
51
|
+
return app.test_client()
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def auth_headers(client, user):
|
|
55
|
+
"""Get auth headers for authenticated requests."""
|
|
56
|
+
response = client.post('/auth/login', json={
|
|
57
|
+
'email': user.email,
|
|
58
|
+
'password': 'password',
|
|
59
|
+
})
|
|
60
|
+
token = response.json['access_token']
|
|
61
|
+
return {'Authorization': f'Bearer {token}'}
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
## Parametrize
|
|
65
|
+
\`\`\`python
|
|
66
|
+
import pytest
|
|
67
|
+
|
|
68
|
+
# Multiple test cases
|
|
69
|
+
@pytest.mark.parametrize("email,expected", [
|
|
70
|
+
("valid@example.com", True),
|
|
71
|
+
("invalid-email", False),
|
|
72
|
+
("", False),
|
|
73
|
+
("a@b.c", True),
|
|
74
|
+
])
|
|
75
|
+
def test_validate_email(email, expected):
|
|
76
|
+
assert validate_email(email) == expected
|
|
77
|
+
|
|
78
|
+
# Multiple parameters
|
|
79
|
+
@pytest.mark.parametrize("a,b,expected", [
|
|
80
|
+
(1, 2, 3),
|
|
81
|
+
(0, 0, 0),
|
|
82
|
+
(-1, 1, 0),
|
|
83
|
+
])
|
|
84
|
+
def test_add(a, b, expected):
|
|
85
|
+
assert add(a, b) == expected
|
|
86
|
+
|
|
87
|
+
# Combining parametrize decorators (cartesian product)
|
|
88
|
+
@pytest.mark.parametrize("x", [1, 2])
|
|
89
|
+
@pytest.mark.parametrize("y", [10, 20])
|
|
90
|
+
def test_multiply(x, y):
|
|
91
|
+
assert multiply(x, y) == x * y
|
|
92
|
+
|
|
93
|
+
# With IDs for better test names
|
|
94
|
+
@pytest.mark.parametrize(
|
|
95
|
+
"status_code,expected_error",
|
|
96
|
+
[
|
|
97
|
+
pytest.param(400, "Bad Request", id="bad_request"),
|
|
98
|
+
pytest.param(401, "Unauthorized", id="unauthorized"),
|
|
99
|
+
pytest.param(404, "Not Found", id="not_found"),
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
def test_error_messages(status_code, expected_error):
|
|
103
|
+
assert get_error_message(status_code) == expected_error
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
## Mocking
|
|
107
|
+
\`\`\`python
|
|
108
|
+
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
|
109
|
+
|
|
110
|
+
# Patch a module function
|
|
111
|
+
@patch('app.services.user.send_email')
|
|
112
|
+
def test_create_user_sends_email(mock_send_email, db_session):
|
|
113
|
+
user = create_user(db_session, "test@example.com", "Test")
|
|
114
|
+
mock_send_email.assert_called_once_with("test@example.com", "Welcome!")
|
|
115
|
+
|
|
116
|
+
# Patch a method
|
|
117
|
+
@patch.object(UserService, 'get_user')
|
|
118
|
+
def test_get_user_not_found(mock_get_user):
|
|
119
|
+
mock_get_user.return_value = None
|
|
120
|
+
result = UserService().get_user(999)
|
|
121
|
+
assert result is None
|
|
122
|
+
|
|
123
|
+
# Mock with return values
|
|
124
|
+
def test_external_api(mocker):
|
|
125
|
+
mock_response = Mock()
|
|
126
|
+
mock_response.json.return_value = {'data': 'value'}
|
|
127
|
+
mock_response.status_code = 200
|
|
128
|
+
|
|
129
|
+
mocker.patch('requests.get', return_value=mock_response)
|
|
130
|
+
|
|
131
|
+
result = fetch_data()
|
|
132
|
+
assert result == {'data': 'value'}
|
|
133
|
+
|
|
134
|
+
# Mock async functions
|
|
135
|
+
@pytest.mark.asyncio
|
|
136
|
+
async def test_async_function(mocker):
|
|
137
|
+
mock_fetch = AsyncMock(return_value={'id': 1})
|
|
138
|
+
mocker.patch('app.services.fetch_user', mock_fetch)
|
|
139
|
+
|
|
140
|
+
result = await get_user_data(1)
|
|
141
|
+
assert result['id'] == 1
|
|
142
|
+
|
|
143
|
+
# Using pytest-mock's mocker fixture
|
|
144
|
+
def test_with_mocker(mocker):
|
|
145
|
+
mocker.patch('time.sleep') # Speed up tests
|
|
146
|
+
mocker.patch('os.environ.get', return_value='test_value')
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Async Testing
|
|
150
|
+
\`\`\`python
|
|
151
|
+
import pytest
|
|
152
|
+
import asyncio
|
|
153
|
+
|
|
154
|
+
# Mark async tests
|
|
155
|
+
@pytest.mark.asyncio
|
|
156
|
+
async def test_async_function():
|
|
157
|
+
result = await async_fetch_data()
|
|
158
|
+
assert result is not None
|
|
159
|
+
|
|
160
|
+
# Async fixture
|
|
161
|
+
@pytest.fixture
|
|
162
|
+
async def async_client():
|
|
163
|
+
async with AsyncClient(app, base_url="http://test") as client:
|
|
164
|
+
yield client
|
|
165
|
+
|
|
166
|
+
@pytest.mark.asyncio
|
|
167
|
+
async def test_async_api(async_client):
|
|
168
|
+
response = await async_client.get("/api/users")
|
|
169
|
+
assert response.status_code == 200
|
|
170
|
+
|
|
171
|
+
# Test async generators
|
|
172
|
+
@pytest.mark.asyncio
|
|
173
|
+
async def test_async_generator():
|
|
174
|
+
results = []
|
|
175
|
+
async for item in async_stream():
|
|
176
|
+
results.append(item)
|
|
177
|
+
assert len(results) == 10
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
180
|
+
## Testing Exceptions
|
|
181
|
+
\`\`\`python
|
|
182
|
+
import pytest
|
|
183
|
+
|
|
184
|
+
def test_raises_exception():
|
|
185
|
+
with pytest.raises(ValueError) as exc_info:
|
|
186
|
+
validate_age(-1)
|
|
187
|
+
assert "Age must be positive" in str(exc_info.value)
|
|
188
|
+
|
|
189
|
+
def test_raises_specific_exception():
|
|
190
|
+
with pytest.raises(ValidationError, match="Invalid email"):
|
|
191
|
+
validate_email("not-an-email")
|
|
192
|
+
|
|
193
|
+
# Test exception attributes
|
|
194
|
+
def test_custom_exception():
|
|
195
|
+
with pytest.raises(AppError) as exc_info:
|
|
196
|
+
do_something_risky()
|
|
197
|
+
|
|
198
|
+
assert exc_info.value.code == "RISK_ERROR"
|
|
199
|
+
assert exc_info.value.status_code == 400
|
|
200
|
+
\`\`\`
|
|
201
|
+
|
|
202
|
+
## Markers
|
|
203
|
+
\`\`\`python
|
|
204
|
+
# Custom markers in pytest.ini or pyproject.toml
|
|
205
|
+
# [tool.pytest.ini_options]
|
|
206
|
+
# markers = [
|
|
207
|
+
# "slow: marks tests as slow",
|
|
208
|
+
# "integration: marks integration tests",
|
|
209
|
+
# ]
|
|
210
|
+
|
|
211
|
+
@pytest.mark.slow
|
|
212
|
+
def test_slow_operation():
|
|
213
|
+
result = process_large_dataset()
|
|
214
|
+
assert result is not None
|
|
215
|
+
|
|
216
|
+
@pytest.mark.integration
|
|
217
|
+
def test_database_connection(db_session):
|
|
218
|
+
assert db_session.execute("SELECT 1").scalar() == 1
|
|
219
|
+
|
|
220
|
+
@pytest.mark.skip(reason="Not implemented yet")
|
|
221
|
+
def test_future_feature():
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
|
|
225
|
+
def test_unix_specific():
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
@pytest.mark.xfail(reason="Known bug #123")
|
|
229
|
+
def test_known_bug():
|
|
230
|
+
assert broken_function() == "expected"
|
|
231
|
+
|
|
232
|
+
# Run specific markers
|
|
233
|
+
# pytest -m slow
|
|
234
|
+
# pytest -m "not slow"
|
|
235
|
+
# pytest -m "integration and not slow"
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
## Coverage Configuration
|
|
239
|
+
\`\`\`toml
|
|
240
|
+
# pyproject.toml
|
|
241
|
+
[tool.pytest.ini_options]
|
|
242
|
+
testpaths = ["tests"]
|
|
243
|
+
python_files = ["test_*.py"]
|
|
244
|
+
python_functions = ["test_*"]
|
|
245
|
+
asyncio_mode = "auto"
|
|
246
|
+
addopts = [
|
|
247
|
+
"-v",
|
|
248
|
+
"--tb=short",
|
|
249
|
+
"--cov=app",
|
|
250
|
+
"--cov-report=term-missing",
|
|
251
|
+
"--cov-report=html",
|
|
252
|
+
"--cov-fail-under=80",
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
[tool.coverage.run]
|
|
256
|
+
source = ["app"]
|
|
257
|
+
omit = ["*/tests/*", "*/__init__.py"]
|
|
258
|
+
branch = true
|
|
259
|
+
|
|
260
|
+
[tool.coverage.report]
|
|
261
|
+
exclude_lines = [
|
|
262
|
+
"pragma: no cover",
|
|
263
|
+
"if TYPE_CHECKING:",
|
|
264
|
+
"raise NotImplementedError",
|
|
265
|
+
]
|
|
266
|
+
\`\`\`
|
|
267
|
+
|
|
268
|
+
## Test Utilities
|
|
269
|
+
\`\`\`python
|
|
270
|
+
# Freeze time
|
|
271
|
+
from freezegun import freeze_time
|
|
272
|
+
|
|
273
|
+
@freeze_time("2024-01-15 12:00:00")
|
|
274
|
+
def test_timestamp():
|
|
275
|
+
assert get_current_time() == datetime(2024, 1, 15, 12, 0, 0)
|
|
276
|
+
|
|
277
|
+
# Factory Boy for test data
|
|
278
|
+
from factory import Factory, Faker, SubFactory
|
|
279
|
+
|
|
280
|
+
class UserFactory(Factory):
|
|
281
|
+
class Meta:
|
|
282
|
+
model = User
|
|
283
|
+
|
|
284
|
+
email = Faker('email')
|
|
285
|
+
name = Faker('name')
|
|
286
|
+
|
|
287
|
+
class PostFactory(Factory):
|
|
288
|
+
class Meta:
|
|
289
|
+
model = Post
|
|
290
|
+
|
|
291
|
+
title = Faker('sentence')
|
|
292
|
+
author = SubFactory(UserFactory)
|
|
293
|
+
|
|
294
|
+
def test_with_factory():
|
|
295
|
+
user = UserFactory()
|
|
296
|
+
posts = PostFactory.create_batch(5, author=user)
|
|
297
|
+
assert len(posts) == 5
|
|
298
|
+
\`\`\`
|
|
299
|
+
|
|
300
|
+
## ❌ DON'T
|
|
301
|
+
- Use global state between tests
|
|
302
|
+
- Skip cleanup in fixtures
|
|
303
|
+
- Write tests that depend on order
|
|
304
|
+
- Mock too much (test real behavior)
|
|
305
|
+
|
|
306
|
+
## ✅ DO
|
|
307
|
+
- Use fixtures for setup/teardown
|
|
308
|
+
- Use parametrize for multiple inputs
|
|
309
|
+
- Use markers to categorize tests
|
|
310
|
+
- Mock external dependencies only
|
|
311
|
+
- Test edge cases and errors
|
|
312
|
+
- Run with coverage reports
|
|
313
|
+
- Use factory patterns for test data
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# Python Skill
|
|
2
|
+
|
|
3
|
+
## Project Structure
|
|
4
|
+
\`\`\`
|
|
5
|
+
my_project/
|
|
6
|
+
├── pyproject.toml # Project config (Poetry/PDM)
|
|
7
|
+
├── .python-version # Python version
|
|
8
|
+
├── .env # Environment variables
|
|
9
|
+
├── src/
|
|
10
|
+
│ └── my_project/
|
|
11
|
+
│ ├── __init__.py
|
|
12
|
+
│ ├── main.py
|
|
13
|
+
│ ├── config.py
|
|
14
|
+
│ ├── models/
|
|
15
|
+
│ ├── services/
|
|
16
|
+
│ └── utils/
|
|
17
|
+
└── tests/
|
|
18
|
+
├── conftest.py
|
|
19
|
+
└── test_*.py
|
|
20
|
+
\`\`\`
|
|
21
|
+
|
|
22
|
+
## Type Hints
|
|
23
|
+
\`\`\`python
|
|
24
|
+
from typing import Optional, List, Dict, TypeVar, Generic
|
|
25
|
+
from collections.abc import Callable, Awaitable
|
|
26
|
+
|
|
27
|
+
# Basic types
|
|
28
|
+
def get_user(user_id: str) -> User | None:
|
|
29
|
+
return db.query(User).filter(User.id == user_id).first()
|
|
30
|
+
|
|
31
|
+
# Collections
|
|
32
|
+
def get_users(ids: list[str]) -> list[User]:
|
|
33
|
+
return db.query(User).filter(User.id.in_(ids)).all()
|
|
34
|
+
|
|
35
|
+
# Dictionaries
|
|
36
|
+
def process_data(data: dict[str, int]) -> dict[str, str]:
|
|
37
|
+
return {k: str(v) for k, v in data.items()}
|
|
38
|
+
|
|
39
|
+
# Callable types
|
|
40
|
+
Handler = Callable[[Request], Awaitable[Response]]
|
|
41
|
+
|
|
42
|
+
# Generics
|
|
43
|
+
T = TypeVar('T')
|
|
44
|
+
|
|
45
|
+
class Repository(Generic[T]):
|
|
46
|
+
def find(self, id: str) -> T | None: ...
|
|
47
|
+
def save(self, entity: T) -> T: ...
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
## Dataclasses & Pydantic
|
|
51
|
+
\`\`\`python
|
|
52
|
+
from dataclasses import dataclass, field
|
|
53
|
+
from pydantic import BaseModel, EmailStr, Field
|
|
54
|
+
|
|
55
|
+
# Dataclasses for simple data containers
|
|
56
|
+
@dataclass
|
|
57
|
+
class User:
|
|
58
|
+
id: str
|
|
59
|
+
email: str
|
|
60
|
+
name: str | None = None
|
|
61
|
+
tags: list[str] = field(default_factory=list)
|
|
62
|
+
|
|
63
|
+
# Pydantic for validation
|
|
64
|
+
class UserCreate(BaseModel):
|
|
65
|
+
email: EmailStr
|
|
66
|
+
name: str = Field(min_length=1, max_length=100)
|
|
67
|
+
password: str = Field(min_length=8)
|
|
68
|
+
|
|
69
|
+
class Config:
|
|
70
|
+
str_strip_whitespace = True
|
|
71
|
+
|
|
72
|
+
# Usage
|
|
73
|
+
user = UserCreate(email="test@example.com", name="John", password="secret123")
|
|
74
|
+
user_dict = user.model_dump()
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
## Async/Await
|
|
78
|
+
\`\`\`python
|
|
79
|
+
import asyncio
|
|
80
|
+
from typing import AsyncIterator
|
|
81
|
+
|
|
82
|
+
# Async function
|
|
83
|
+
async def fetch_user(user_id: str) -> User:
|
|
84
|
+
async with httpx.AsyncClient() as client:
|
|
85
|
+
response = await client.get(f"/users/{user_id}")
|
|
86
|
+
return User(**response.json())
|
|
87
|
+
|
|
88
|
+
# Concurrent execution
|
|
89
|
+
async def fetch_all_users(ids: list[str]) -> list[User]:
|
|
90
|
+
tasks = [fetch_user(id) for id in ids]
|
|
91
|
+
return await asyncio.gather(*tasks)
|
|
92
|
+
|
|
93
|
+
# Async context manager
|
|
94
|
+
class DatabaseConnection:
|
|
95
|
+
async def __aenter__(self) -> "DatabaseConnection":
|
|
96
|
+
await self.connect()
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def __aexit__(self, *args) -> None:
|
|
100
|
+
await self.disconnect()
|
|
101
|
+
|
|
102
|
+
# Async generator
|
|
103
|
+
async def stream_users() -> AsyncIterator[User]:
|
|
104
|
+
async for row in db.stream_query("SELECT * FROM users"):
|
|
105
|
+
yield User(**row)
|
|
106
|
+
|
|
107
|
+
# Usage
|
|
108
|
+
async for user in stream_users():
|
|
109
|
+
print(user.name)
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
112
|
+
## Error Handling
|
|
113
|
+
\`\`\`python
|
|
114
|
+
from typing import NoReturn
|
|
115
|
+
|
|
116
|
+
# Custom exceptions
|
|
117
|
+
class UserNotFoundError(Exception):
|
|
118
|
+
def __init__(self, user_id: str):
|
|
119
|
+
self.user_id = user_id
|
|
120
|
+
super().__init__(f"User {user_id} not found")
|
|
121
|
+
|
|
122
|
+
class ValidationError(Exception):
|
|
123
|
+
def __init__(self, errors: list[str]):
|
|
124
|
+
self.errors = errors
|
|
125
|
+
super().__init__(f"Validation failed: {errors}")
|
|
126
|
+
|
|
127
|
+
# Try/except patterns
|
|
128
|
+
def get_user_or_raise(user_id: str) -> User:
|
|
129
|
+
try:
|
|
130
|
+
user = db.get_user(user_id)
|
|
131
|
+
if not user:
|
|
132
|
+
raise UserNotFoundError(user_id)
|
|
133
|
+
return user
|
|
134
|
+
except DatabaseError as e:
|
|
135
|
+
logger.error(f"Database error: {e}")
|
|
136
|
+
raise
|
|
137
|
+
finally:
|
|
138
|
+
db.close()
|
|
139
|
+
|
|
140
|
+
# Result pattern (no exceptions)
|
|
141
|
+
from dataclasses import dataclass
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class Result[T]:
|
|
145
|
+
value: T | None = None
|
|
146
|
+
error: str | None = None
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def is_ok(self) -> bool:
|
|
150
|
+
return self.error is None
|
|
151
|
+
|
|
152
|
+
def safe_get_user(user_id: str) -> Result[User]:
|
|
153
|
+
try:
|
|
154
|
+
user = db.get_user(user_id)
|
|
155
|
+
return Result(value=user)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return Result(error=str(e))
|
|
158
|
+
\`\`\`
|
|
159
|
+
|
|
160
|
+
## Logging
|
|
161
|
+
\`\`\`python
|
|
162
|
+
import logging
|
|
163
|
+
from logging.handlers import RotatingFileHandler
|
|
164
|
+
|
|
165
|
+
# Configure logging
|
|
166
|
+
logging.basicConfig(
|
|
167
|
+
level=logging.INFO,
|
|
168
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Module logger
|
|
172
|
+
logger = logging.getLogger(__name__)
|
|
173
|
+
|
|
174
|
+
# Structured logging
|
|
175
|
+
logger.info("User created", extra={"user_id": user.id, "email": user.email})
|
|
176
|
+
|
|
177
|
+
# Production setup
|
|
178
|
+
def setup_logging():
|
|
179
|
+
handler = RotatingFileHandler(
|
|
180
|
+
"app.log",
|
|
181
|
+
maxBytes=10_000_000, # 10MB
|
|
182
|
+
backupCount=5,
|
|
183
|
+
)
|
|
184
|
+
handler.setFormatter(logging.Formatter(
|
|
185
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
186
|
+
))
|
|
187
|
+
logging.getLogger().addHandler(handler)
|
|
188
|
+
\`\`\`
|
|
189
|
+
|
|
190
|
+
## Context Managers
|
|
191
|
+
\`\`\`python
|
|
192
|
+
from contextlib import contextmanager, asynccontextmanager
|
|
193
|
+
|
|
194
|
+
# Sync context manager
|
|
195
|
+
@contextmanager
|
|
196
|
+
def transaction():
|
|
197
|
+
db.begin()
|
|
198
|
+
try:
|
|
199
|
+
yield db
|
|
200
|
+
db.commit()
|
|
201
|
+
except Exception:
|
|
202
|
+
db.rollback()
|
|
203
|
+
raise
|
|
204
|
+
|
|
205
|
+
# Async context manager
|
|
206
|
+
@asynccontextmanager
|
|
207
|
+
async def get_connection():
|
|
208
|
+
conn = await pool.acquire()
|
|
209
|
+
try:
|
|
210
|
+
yield conn
|
|
211
|
+
finally:
|
|
212
|
+
await pool.release(conn)
|
|
213
|
+
|
|
214
|
+
# Usage
|
|
215
|
+
async with get_connection() as conn:
|
|
216
|
+
await conn.execute("SELECT * FROM users")
|
|
217
|
+
\`\`\`
|
|
218
|
+
|
|
219
|
+
## Environment & Config
|
|
220
|
+
\`\`\`python
|
|
221
|
+
from pydantic_settings import BaseSettings
|
|
222
|
+
from functools import lru_cache
|
|
223
|
+
|
|
224
|
+
class Settings(BaseSettings):
|
|
225
|
+
database_url: str
|
|
226
|
+
redis_url: str
|
|
227
|
+
jwt_secret: str
|
|
228
|
+
debug: bool = False
|
|
229
|
+
|
|
230
|
+
class Config:
|
|
231
|
+
env_file = ".env"
|
|
232
|
+
|
|
233
|
+
@lru_cache
|
|
234
|
+
def get_settings() -> Settings:
|
|
235
|
+
return Settings()
|
|
236
|
+
|
|
237
|
+
# Usage
|
|
238
|
+
settings = get_settings()
|
|
239
|
+
print(settings.database_url)
|
|
240
|
+
\`\`\`
|
|
241
|
+
|
|
242
|
+
## Virtual Environments
|
|
243
|
+
\`\`\`bash
|
|
244
|
+
# Create venv
|
|
245
|
+
python -m venv .venv
|
|
246
|
+
|
|
247
|
+
# Activate
|
|
248
|
+
source .venv/bin/activate # Unix
|
|
249
|
+
.venv\\Scripts\\activate # Windows
|
|
250
|
+
|
|
251
|
+
# Modern: use Poetry or PDM
|
|
252
|
+
poetry init
|
|
253
|
+
poetry add fastapi uvicorn
|
|
254
|
+
poetry install
|
|
255
|
+
\`\`\`
|
|
256
|
+
|
|
257
|
+
## ❌ DON'T
|
|
258
|
+
- Use mutable default arguments (use None or field())
|
|
259
|
+
- Catch bare Exception without re-raising
|
|
260
|
+
- Use global state without proper management
|
|
261
|
+
- Mix sync and async code incorrectly
|
|
262
|
+
- Ignore type hints
|
|
263
|
+
|
|
264
|
+
## ✅ DO
|
|
265
|
+
- Follow PEP 8 (use ruff/black for formatting)
|
|
266
|
+
- Use type hints everywhere
|
|
267
|
+
- Prefer f-strings over .format()
|
|
268
|
+
- Use context managers for resources
|
|
269
|
+
- Use dataclasses/pydantic for data
|
|
270
|
+
- Use async for I/O-bound operations
|
|
271
|
+
- Use logging instead of print
|
|
272
|
+
- Use virtual environments
|