python-skills 1.0.0__py3-none-any.whl
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.
- python_skills/__init__.py +10 -0
- python_skills/__main__.py +6 -0
- python_skills/adapters/__init__.py +48 -0
- python_skills/adapters/agent_skills.py +415 -0
- python_skills/adapters/aider_adapter.py +226 -0
- python_skills/adapters/base.py +153 -0
- python_skills/adapters/claude.py +474 -0
- python_skills/adapters/cline.py +332 -0
- python_skills/adapters/codex.py +24 -0
- python_skills/adapters/continue_adapter.py +198 -0
- python_skills/adapters/cursor.py +327 -0
- python_skills/adapters/gemini.py +26 -0
- python_skills/adapters/goose.py +26 -0
- python_skills/adapters/junie.py +25 -0
- python_skills/adapters/kiro.py +382 -0
- python_skills/adapters/opencode.py +27 -0
- python_skills/adapters/roo.py +25 -0
- python_skills/adapters/universal.py +203 -0
- python_skills/adapters/vscode.py +27 -0
- python_skills/adapters/windsurf.py +26 -0
- python_skills/adapters/zed.py +27 -0
- python_skills/cli.py +326 -0
- python_skills/config.py +160 -0
- python_skills/detector.py +152 -0
- python_skills/installer.py +163 -0
- python_skills/markers.py +115 -0
- python_skills/skills/__init__.py +14 -0
- python_skills/skills/loader.py +171 -0
- python_skills/skills/metadata.py +152 -0
- python_skills/skills/registry.py +101 -0
- python_skills/state.py +204 -0
- python_skills-1.0.0.dist-info/METADATA +99 -0
- python_skills-1.0.0.dist-info/RECORD +105 -0
- python_skills-1.0.0.dist-info/WHEEL +4 -0
- python_skills-1.0.0.dist-info/entry_points.txt +2 -0
- python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
- skills/advanced_python.md +239 -0
- skills/anti_patterns/index.md +406 -0
- skills/comprehensions.md +167 -0
- skills/control_flow.md +175 -0
- skills/data_structures.md +243 -0
- skills/debugging/common_bugs.md +222 -0
- skills/debugging/inspection_techniques.md +249 -0
- skills/debugging/root_cause.md +203 -0
- skills/engineering/application_logging.md +195 -0
- skills/engineering/cli_apps.md +207 -0
- skills/engineering/configuration.md +218 -0
- skills/engineering/database.md +240 -0
- skills/engineering/dependency_management.md +205 -0
- skills/engineering/http_clients.md +267 -0
- skills/engineering/modules_packages.md +211 -0
- skills/engineering/packaging.md +197 -0
- skills/engineering/project_structure.md +155 -0
- skills/engineering/pyproject_toml.md +302 -0
- skills/engineering/virtual_environments.md +206 -0
- skills/functions.md +244 -0
- skills/generation/async_concurrency.md +291 -0
- skills/generation/error_handling.md +276 -0
- skills/generation/protocols_generics.md +243 -0
- skills/generation/type_hints.md +290 -0
- skills/generation/validation_pipeline.md +274 -0
- skills/generation/workflow.md +190 -0
- skills/oop.md +228 -0
- skills/quality/abstractions.md +154 -0
- skills/quality/comments.md +177 -0
- skills/quality/documentation.md +176 -0
- skills/quality/duplication.md +137 -0
- skills/quality/maintainability.md +142 -0
- skills/quality/naming.md +171 -0
- skills/quality/quality_functions.md +245 -0
- skills/quality/readability.md +239 -0
- skills/quality/type_annotations.md +192 -0
- skills/refactoring/behavior_preservation.md +157 -0
- skills/refactoring/incremental.md +187 -0
- skills/refactoring/interface_stability.md +199 -0
- skills/refactoring/safe_refactoring.md +206 -0
- skills/security/auth_boundaries.md +200 -0
- skills/security/command_injection.md +207 -0
- skills/security/dependency_risks.md +282 -0
- skills/security/file_handling.md +156 -0
- skills/security/input_validation.md +190 -0
- skills/security/path_traversal.md +172 -0
- skills/security/secrets.md +171 -0
- skills/security/sql_injection.md +188 -0
- skills/security/unsafe_deserialization.md +164 -0
- skills/stdlib/argparse.md +178 -0
- skills/stdlib/collections.md +212 -0
- skills/stdlib/datetime.md +187 -0
- skills/stdlib/functools.md +238 -0
- skills/stdlib/itertools.md +183 -0
- skills/stdlib/json.md +162 -0
- skills/stdlib/logging.md +185 -0
- skills/stdlib/os_sys.md +184 -0
- skills/stdlib/pathlib.md +218 -0
- skills/stdlib/re.md +171 -0
- skills/stdlib/statistics.md +112 -0
- skills/stdlib/subprocess.md +211 -0
- skills/testing/async_tests.md +249 -0
- skills/testing/coverage.md +168 -0
- skills/testing/edge_cases.md +197 -0
- skills/testing/fixtures_mocks.md +203 -0
- skills/testing/organization.md +205 -0
- skills/testing/parameterized.md +174 -0
- skills/testing/regression_tests.md +165 -0
- skills/variables_types.md +107 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engineering_database
|
|
3
|
+
purpose: Database access patterns and best practices
|
|
4
|
+
category: engineering
|
|
5
|
+
triggers:
|
|
6
|
+
- database
|
|
7
|
+
- sql
|
|
8
|
+
- query
|
|
9
|
+
- orm
|
|
10
|
+
- sqlite
|
|
11
|
+
- postgresql
|
|
12
|
+
- mysql
|
|
13
|
+
- migration
|
|
14
|
+
dependencies:
|
|
15
|
+
- security/sql_injection.md
|
|
16
|
+
- engineering/configuration.md
|
|
17
|
+
- generation/async_concurrency.md
|
|
18
|
+
- generation/error_handling.md
|
|
19
|
+
- testing/organization.md
|
|
20
|
+
priority: primary
|
|
21
|
+
estimated_tokens: 2200
|
|
22
|
+
---
|
|
23
|
+
# Engineering: Database
|
|
24
|
+
|
|
25
|
+
**Purpose**: Database access patterns and best practices.
|
|
26
|
+
|
|
27
|
+
**When to use**: Any code interacting with databases.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Core Rules
|
|
32
|
+
|
|
33
|
+
### Driver Selection
|
|
34
|
+
| Database | Async Driver | Sync Driver |
|
|
35
|
+
|----------|--------------|-------------|
|
|
36
|
+
| PostgreSQL | `asyncpg` | `psycopg` (v3) |
|
|
37
|
+
| MySQL | `aiomysql` | `pymysql` |
|
|
38
|
+
| SQLite | `aiosqlite` | `sqlite3` (stdlib) |
|
|
39
|
+
| MongoDB | `motor` | `pymongo` |
|
|
40
|
+
|
|
41
|
+
### Connection Management
|
|
42
|
+
```python
|
|
43
|
+
# Async (asyncpg example)
|
|
44
|
+
import asyncpg
|
|
45
|
+
|
|
46
|
+
pool: asyncpg.Pool = await asyncpg.create_pool(
|
|
47
|
+
dsn="postgresql://user:pass@host/db",
|
|
48
|
+
min_size=5,
|
|
49
|
+
max_size=20,
|
|
50
|
+
command_timeout=30,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
async def get_user(user_id: int) -> User | None:
|
|
54
|
+
async with pool.acquire() as conn:
|
|
55
|
+
row = await conn.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
|
|
56
|
+
return User(**row) if row else None
|
|
57
|
+
|
|
58
|
+
# Sync (psycopg)
|
|
59
|
+
import psycopg
|
|
60
|
+
from psycopg_pool import ConnectionPool
|
|
61
|
+
|
|
62
|
+
pool = ConnectionPool("postgresql://user:pass@host/db", min_size=5, max_size=20)
|
|
63
|
+
|
|
64
|
+
def get_user(user_id: int) -> User | None:
|
|
65
|
+
with pool.connection() as conn:
|
|
66
|
+
with conn.cursor() as cur:
|
|
67
|
+
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
68
|
+
row = cur.fetchone()
|
|
69
|
+
return User(*row) if row else None
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Parameterized Queries (SQL Injection Prevention)
|
|
73
|
+
```python
|
|
74
|
+
# CORRECT — parameterized
|
|
75
|
+
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
|
|
76
|
+
cursor.execute("SELECT * FROM users WHERE id = $1", (user_id,)) # asyncpg
|
|
77
|
+
|
|
78
|
+
# WRONG — string interpolation
|
|
79
|
+
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'") # INJECTION!
|
|
80
|
+
cursor.execute("SELECT * FROM users WHERE id = " + str(user_id)) # INJECTION!
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### ORM (SQLAlchemy 2.0+)
|
|
84
|
+
```python
|
|
85
|
+
from sqlalchemy import select
|
|
86
|
+
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
87
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
88
|
+
|
|
89
|
+
class Base(DeclarativeBase):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
class User(Base):
|
|
93
|
+
__tablename__ = "users"
|
|
94
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
95
|
+
email: Mapped[str] = mapped_column(unique=True)
|
|
96
|
+
name: Mapped[str]
|
|
97
|
+
|
|
98
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@host/db")
|
|
99
|
+
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
|
100
|
+
|
|
101
|
+
async def get_user(session: AsyncSession, user_id: int) -> User | None:
|
|
102
|
+
return await session.get(User, user_id)
|
|
103
|
+
|
|
104
|
+
async def create_user(session: AsyncSession, email: str, name: str) -> User:
|
|
105
|
+
user = User(email=email, name=name)
|
|
106
|
+
session.add(user)
|
|
107
|
+
await session.commit()
|
|
108
|
+
return user
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Migrations (Alembic)
|
|
112
|
+
```bash
|
|
113
|
+
# Initialize
|
|
114
|
+
alembic init alembic
|
|
115
|
+
|
|
116
|
+
# Create migration
|
|
117
|
+
alembic revision --autogenerate -m "Add users table"
|
|
118
|
+
|
|
119
|
+
# Apply
|
|
120
|
+
alembic upgrade head
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Repository Pattern
|
|
124
|
+
```python
|
|
125
|
+
from abc import ABC, abstractmethod
|
|
126
|
+
from typing import Protocol
|
|
127
|
+
|
|
128
|
+
class UserRepository(Protocol):
|
|
129
|
+
async def get(self, user_id: int) -> User | None: ...
|
|
130
|
+
async def get_by_email(self, email: str) -> User | None: ...
|
|
131
|
+
async def save(self, user: User) -> User: ...
|
|
132
|
+
async def delete(self, user_id: int) -> bool: ...
|
|
133
|
+
|
|
134
|
+
class PostgresUserRepository:
|
|
135
|
+
def __init__(self, pool: asyncpg.Pool):
|
|
136
|
+
self.pool = pool
|
|
137
|
+
|
|
138
|
+
async def get(self, user_id: int) -> User | None:
|
|
139
|
+
async with self.pool.acquire() as conn:
|
|
140
|
+
row = await conn.fetchrow("SELECT * FROM users WHERE id = $1", user_id)
|
|
141
|
+
return User(**row) if row else None
|
|
142
|
+
|
|
143
|
+
# ... implement other methods
|
|
144
|
+
|
|
145
|
+
# Usage (dependency injection)
|
|
146
|
+
async def handler(repo: UserRepository, user_id: int):
|
|
147
|
+
user = await repo.get(user_id)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Transactions
|
|
151
|
+
```python
|
|
152
|
+
# Async
|
|
153
|
+
async with pool.acquire() as conn:
|
|
154
|
+
async with conn.transaction():
|
|
155
|
+
await conn.execute("INSERT ...")
|
|
156
|
+
await conn.execute("UPDATE ...")
|
|
157
|
+
|
|
158
|
+
# SQLAlchemy
|
|
159
|
+
async with async_session() as session:
|
|
160
|
+
async with session.begin():
|
|
161
|
+
session.add(user1)
|
|
162
|
+
session.add(user2)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Decision Rules
|
|
168
|
+
|
|
169
|
+
| Need | Approach |
|
|
170
|
+
|------|----------|
|
|
171
|
+
| Simple queries, performance critical | Raw driver (`asyncpg`, `psycopg`) |
|
|
172
|
+
| Complex domain model, migrations | SQLAlchemy + Alembic |
|
|
173
|
+
| Document data | MongoDB (`motor`/`pymongo`) |
|
|
174
|
+
| Embedded/local | SQLite (`aiosqlite`/`sqlite3`) |
|
|
175
|
+
| Testing | In-memory SQLite or testcontainers |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Preferred Patterns
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# Dependency injection for testability
|
|
183
|
+
class Database:
|
|
184
|
+
def __init__(self, pool: asyncpg.Pool):
|
|
185
|
+
self.pool = pool
|
|
186
|
+
self.users = PostgresUserRepository(pool)
|
|
187
|
+
self.orders = PostgresOrderRepository(pool)
|
|
188
|
+
|
|
189
|
+
# Context manager for transactions
|
|
190
|
+
@asynccontextmanager
|
|
191
|
+
async def transaction(db: Database):
|
|
192
|
+
async with db.pool.acquire() as conn:
|
|
193
|
+
async with conn.transaction():
|
|
194
|
+
yield conn
|
|
195
|
+
|
|
196
|
+
# Type-safe queries with dataclasses
|
|
197
|
+
from dataclasses import dataclass
|
|
198
|
+
|
|
199
|
+
@dataclass
|
|
200
|
+
class UserRow:
|
|
201
|
+
id: int
|
|
202
|
+
email: str
|
|
203
|
+
name: str
|
|
204
|
+
|
|
205
|
+
async def get_user(conn, user_id: int) -> UserRow | None:
|
|
206
|
+
row = await conn.fetchrow("SELECT id, email, name FROM users WHERE id = $1", user_id)
|
|
207
|
+
return UserRow(**row) if row else None
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Avoid
|
|
213
|
+
|
|
214
|
+
- String interpolation in SQL (injection!)
|
|
215
|
+
- Global connection/pool (use DI)
|
|
216
|
+
- Long-running transactions
|
|
217
|
+
- N+1 queries (use joins or batch loading)
|
|
218
|
+
- ORM for everything (raw SQL for complex queries)
|
|
219
|
+
- No connection pooling
|
|
220
|
+
- Committing in loops
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Validation Considerations
|
|
225
|
+
|
|
226
|
+
- Test with real database (testcontainers)
|
|
227
|
+
- Verify parameterized queries used everywhere
|
|
228
|
+
- Check connection pool sizing
|
|
229
|
+
- Test migration up/down
|
|
230
|
+
- Load test connection limits
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Related Skills
|
|
235
|
+
|
|
236
|
+
- `security/sql_injection.md`
|
|
237
|
+
- `engineering/configuration.md`
|
|
238
|
+
- `generation/async_concurrency.md`
|
|
239
|
+
- `generation/error_handling.md`
|
|
240
|
+
- `testing/organization.md`
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Engineering: Dependency Management
|
|
2
|
+
|
|
3
|
+
**Purpose**: Rules for adding, updating, and managing dependencies.
|
|
4
|
+
|
|
5
|
+
**When to use**: Any time a new import or requirement is considered.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Core Rules
|
|
10
|
+
|
|
11
|
+
### Dependency Decision Tree
|
|
12
|
+
```
|
|
13
|
+
Can stdlib solve this?
|
|
14
|
+
↓ YES → Use stdlib
|
|
15
|
+
↓ NO
|
|
16
|
+
Is an existing project dependency suitable?
|
|
17
|
+
↓ YES → Use existing
|
|
18
|
+
↓ NO
|
|
19
|
+
Is a new dependency justified?
|
|
20
|
+
↓ YES → Add with justification
|
|
21
|
+
↓ NO → Reconsider approach
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Justification Criteria for New Dependencies
|
|
25
|
+
1. **Functionality gap** — stdlib + existing deps cannot reasonably solve
|
|
26
|
+
2. **Maintenance** — Actively maintained (recent releases, responsive issues)
|
|
27
|
+
3. **Security** — No known unpatched vulnerabilities
|
|
28
|
+
4. **Compatibility** — Supports project's Python version range
|
|
29
|
+
5. **License** — Compatible with project license
|
|
30
|
+
6. **Size** — Reasonable install size, minimal transitive deps
|
|
31
|
+
7. **Alternatives evaluated** — Considered lighter alternatives
|
|
32
|
+
|
|
33
|
+
### Version Constraints
|
|
34
|
+
```toml
|
|
35
|
+
# Libraries: compatible ranges
|
|
36
|
+
dependencies = [
|
|
37
|
+
"requests>=2.31,<3", # Semver major
|
|
38
|
+
"pydantic>=2.0,<3",
|
|
39
|
+
"click>=8.0,<9",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Applications: lock files
|
|
43
|
+
# requirements.txt / uv.lock / poetry.lock / pdm.lock
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Dependency Categories
|
|
47
|
+
| Category | Purpose | Version Policy |
|
|
48
|
+
|----------|---------|----------------|
|
|
49
|
+
| Runtime | Required for library/app to work | Compatible range |
|
|
50
|
+
| Optional | Extra features | Compatible range |
|
|
51
|
+
| Dev | Testing, linting, docs | Pinned or loose |
|
|
52
|
+
| Build | Build backend, generators | Pinned |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Lock Files (Applications)
|
|
57
|
+
|
|
58
|
+
### For Applications (Not Libraries)
|
|
59
|
+
```bash
|
|
60
|
+
# uv (fast, modern)
|
|
61
|
+
uv pip compile pyproject.toml -o requirements.txt
|
|
62
|
+
uv sync
|
|
63
|
+
|
|
64
|
+
# pip-tools
|
|
65
|
+
pip-compile pyproject.toml -o requirements.txt
|
|
66
|
+
pip-sync requirements.txt
|
|
67
|
+
|
|
68
|
+
# poetry
|
|
69
|
+
poetry lock
|
|
70
|
+
poetry install
|
|
71
|
+
|
|
72
|
+
# pdm
|
|
73
|
+
pdm lock
|
|
74
|
+
pdm install
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Lock File Policy
|
|
78
|
+
- **Libraries**: No lock file in repo (only `pyproject.toml`)
|
|
79
|
+
- **Applications**: Lock file committed (`requirements.txt`, `uv.lock`, `poetry.lock`)
|
|
80
|
+
- **CI**: Use lock file for reproducible builds
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Dependency Security
|
|
85
|
+
|
|
86
|
+
### Scanning
|
|
87
|
+
```bash
|
|
88
|
+
# Safety (checks known vulnerabilities)
|
|
89
|
+
safety check
|
|
90
|
+
|
|
91
|
+
# pip-audit
|
|
92
|
+
pip-audit
|
|
93
|
+
|
|
94
|
+
# GitHub Dependabot / GitLab Dependency Scanning
|
|
95
|
+
# Configured in CI
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Update Policy
|
|
99
|
+
```bash
|
|
100
|
+
# Check outdated
|
|
101
|
+
uv pip list --outdated
|
|
102
|
+
pip list --outdated
|
|
103
|
+
|
|
104
|
+
# Update (test first!)
|
|
105
|
+
uv pip install --upgrade package
|
|
106
|
+
# Run full test suite
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Vendoring (Last Resort)
|
|
112
|
+
```python
|
|
113
|
+
# Only when:
|
|
114
|
+
# - Dependency abandoned
|
|
115
|
+
# - Need modification
|
|
116
|
+
# - Cannot depend on external (air-gapped)
|
|
117
|
+
|
|
118
|
+
# Structure
|
|
119
|
+
src/
|
|
120
|
+
└── mypackage/
|
|
121
|
+
├── _vendor/
|
|
122
|
+
│ └── requests/ # Copied source
|
|
123
|
+
│ └── __init__.py # from _vendor.requests import ...
|
|
124
|
+
└── ...
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Decision Rules
|
|
130
|
+
|
|
131
|
+
| Situation | Action |
|
|
132
|
+
|-----------|--------|
|
|
133
|
+
| Stdlib has it | Use stdlib |
|
|
134
|
+
| Existing dep has it | Use existing |
|
|
135
|
+
| Simple utility (few lines) | Copy code (with license) |
|
|
136
|
+
| Complex, well-maintained lib | Add dependency |
|
|
137
|
+
| Abandoned dependency | Fork/vendor or replace |
|
|
138
|
+
| Security vulnerability | Upgrade immediately |
|
|
139
|
+
| License conflict | Find alternative |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Preferred Patterns
|
|
144
|
+
|
|
145
|
+
```toml
|
|
146
|
+
# pyproject.toml - clear separation
|
|
147
|
+
[project]
|
|
148
|
+
dependencies = [
|
|
149
|
+
# Core runtime deps
|
|
150
|
+
"httpx>=0.25,<1",
|
|
151
|
+
"pydantic>=2.0,<3",
|
|
152
|
+
"python-dotenv>=1.0,<2",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
[project.optional-dependencies]
|
|
156
|
+
# Optional features
|
|
157
|
+
async = ["aiohttp>=3.8,<4"]
|
|
158
|
+
redis = ["redis>=5.0,<6"]
|
|
159
|
+
postgres = ["asyncpg>=0.29,<1", "sqlalchemy>=2.0,<3"]
|
|
160
|
+
|
|
161
|
+
# Dev tools (not installed by users)
|
|
162
|
+
dev = [
|
|
163
|
+
"pytest>=7.4,<8",
|
|
164
|
+
"pytest-asyncio>=0.21,<1",
|
|
165
|
+
"pytest-cov>=4.1,<5",
|
|
166
|
+
"ruff>=0.5,<1",
|
|
167
|
+
"mypy>=1.10,<2",
|
|
168
|
+
"pre-commit>=3.0,<4",
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
# Build deps (in build-system)
|
|
172
|
+
[build-system]
|
|
173
|
+
requires = ["hatchling"]
|
|
174
|
+
build-backend = "hatchling.build"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Avoid
|
|
180
|
+
|
|
181
|
+
- Adding deps for trivial functionality (`left-pad` style)
|
|
182
|
+
- Pinning exact versions in library `pyproject.toml` (`==1.2.3`)
|
|
183
|
+
- No upper bounds in library deps (breaking changes)
|
|
184
|
+
- Commiting `requirements.txt` for libraries
|
|
185
|
+
- Ignoring security advisories
|
|
186
|
+
- Transitive dependency hell (audit `pipdeptree`)
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Validation Considerations
|
|
191
|
+
|
|
192
|
+
- `pipdeptree` shows dependency graph
|
|
193
|
+
- `pip check` validates consistency
|
|
194
|
+
- `safety check` / `pip-audit` clean
|
|
195
|
+
- License check: `pip-licenses`
|
|
196
|
+
- Build reproducibility: `pip install` from lock file
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Related Skills
|
|
201
|
+
|
|
202
|
+
- `engineering/pyproject_toml.md`
|
|
203
|
+
- `engineering/packaging.md`
|
|
204
|
+
- `engineering/virtual_environments.md`
|
|
205
|
+
- `security/dependency_risks.md`
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engineering_http_clients
|
|
3
|
+
purpose: HTTP client selection and usage patterns
|
|
4
|
+
category: engineering
|
|
5
|
+
triggers:
|
|
6
|
+
- http
|
|
7
|
+
- request
|
|
8
|
+
- client
|
|
9
|
+
- api
|
|
10
|
+
- rest
|
|
11
|
+
- webhook
|
|
12
|
+
- download
|
|
13
|
+
dependencies:
|
|
14
|
+
- generation/async_concurrency.md
|
|
15
|
+
- generation/error_handling.md
|
|
16
|
+
- engineering/configuration.md
|
|
17
|
+
- security/secrets.md
|
|
18
|
+
- testing/organization.md
|
|
19
|
+
priority: primary
|
|
20
|
+
estimated_tokens: 2500
|
|
21
|
+
---
|
|
22
|
+
# Engineering: HTTP Clients
|
|
23
|
+
|
|
24
|
+
**Purpose**: HTTP client selection and usage patterns.
|
|
25
|
+
|
|
26
|
+
**When to use**: Making HTTP requests (APIs, webhooks, downloads).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Core Rules
|
|
31
|
+
|
|
32
|
+
### Client Selection
|
|
33
|
+
|
|
34
|
+
| Need | Client |
|
|
35
|
+
|------|--------|
|
|
36
|
+
| Sync, simple | `requests` |
|
|
37
|
+
| Sync + HTTP/2 | `httpx` (sync) |
|
|
38
|
+
| Async | `httpx` / `aiohttp` |
|
|
39
|
+
| Stdlib only | `urllib.request` (avoid) |
|
|
40
|
+
| Testing | `respx` / `pytest-httpx` / `requests-mock` |
|
|
41
|
+
|
|
42
|
+
### httpx (Recommended — Sync + Async)
|
|
43
|
+
```python
|
|
44
|
+
import httpx
|
|
45
|
+
|
|
46
|
+
# Sync
|
|
47
|
+
client = httpx.Client(timeout=30.0, limits=httpx.Limits(max_connections=100))
|
|
48
|
+
response = client.get("https://api.example.com/users")
|
|
49
|
+
client.close()
|
|
50
|
+
|
|
51
|
+
# Context manager
|
|
52
|
+
with httpx.Client() as client:
|
|
53
|
+
response = client.get(url)
|
|
54
|
+
|
|
55
|
+
# Async
|
|
56
|
+
async with httpx.AsyncClient() as client:
|
|
57
|
+
response = await client.get(url)
|
|
58
|
+
|
|
59
|
+
# Common options
|
|
60
|
+
client = httpx.Client(
|
|
61
|
+
base_url="https://api.example.com",
|
|
62
|
+
headers={"User-Agent": "MyApp/1.0"},
|
|
63
|
+
timeout=httpx.Timeout(connect=5.0, read=30.0),
|
|
64
|
+
follow_redirects=True,
|
|
65
|
+
limits=httpx.Limits(max_keepalive_connections=20),
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Response Handling
|
|
70
|
+
```python
|
|
71
|
+
response = client.get(url)
|
|
72
|
+
|
|
73
|
+
# Status
|
|
74
|
+
response.status_code
|
|
75
|
+
response.is_success
|
|
76
|
+
response.raise_for_status() # Raises HTTPStatusError
|
|
77
|
+
|
|
78
|
+
# Content
|
|
79
|
+
response.text # str (decoded)
|
|
80
|
+
response.content # bytes
|
|
81
|
+
response.json() # Parsed JSON
|
|
82
|
+
response.iter_bytes() # Stream
|
|
83
|
+
|
|
84
|
+
# Headers
|
|
85
|
+
response.headers["Content-Type"]
|
|
86
|
+
response.headers.get("X-Custom")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Error Handling
|
|
90
|
+
```python
|
|
91
|
+
import httpx
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
response = client.get(url, timeout=10.0)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
except httpx.TimeoutException:
|
|
97
|
+
logger.warning("Request timed out")
|
|
98
|
+
raise ServiceTimeout()
|
|
99
|
+
except httpx.ConnectError:
|
|
100
|
+
logger.warning("Connection failed")
|
|
101
|
+
raise ServiceUnavailable()
|
|
102
|
+
except httpx.HTTPStatusError as e:
|
|
103
|
+
if e.response.status_code == 404:
|
|
104
|
+
raise NotFoundError()
|
|
105
|
+
elif e.response.status_code == 429:
|
|
106
|
+
raise RateLimited(retry_after=e.response.headers.get("Retry-After"))
|
|
107
|
+
else:
|
|
108
|
+
logger.error("HTTP error", status=e.response.status_code)
|
|
109
|
+
raise ServiceError()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Retry Logic
|
|
113
|
+
```python
|
|
114
|
+
from httpx import Retry
|
|
115
|
+
from httpx._transports.default import RetryStrategy
|
|
116
|
+
|
|
117
|
+
# Built-in retry (httpx 0.25+)
|
|
118
|
+
transport = httpx.HTTPTransport(
|
|
119
|
+
retries=Retry(
|
|
120
|
+
total=3,
|
|
121
|
+
backoff_factor=0.5,
|
|
122
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
123
|
+
allowed_methods=["HEAD", "GET", "OPTIONS"],
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
client = httpx.Client(transport=transport)
|
|
127
|
+
|
|
128
|
+
# Or use tenacity for complex retry
|
|
129
|
+
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
130
|
+
|
|
131
|
+
@retry(
|
|
132
|
+
wait=wait_exponential(multiplier=1, min=1, max=10),
|
|
133
|
+
stop=stop_after_attempt(3),
|
|
134
|
+
retry=retry_if_exception_type(httpx.RequestError),
|
|
135
|
+
)
|
|
136
|
+
async def fetch_with_retry(client: httpx.AsyncClient, url: str) -> httpx.Response:
|
|
137
|
+
return await client.get(url)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Authentication
|
|
141
|
+
```python
|
|
142
|
+
# Bearer token
|
|
143
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
144
|
+
|
|
145
|
+
# Basic auth
|
|
146
|
+
auth = httpx.BasicAuth("user", "pass")
|
|
147
|
+
|
|
148
|
+
# Custom auth
|
|
149
|
+
class APIKeyAuth(httpx.Auth):
|
|
150
|
+
def __init__(self, api_key: str):
|
|
151
|
+
self.api_key = api_key
|
|
152
|
+
|
|
153
|
+
def auth_flow(self, request):
|
|
154
|
+
request.headers["X-API-Key"] = self.api_key
|
|
155
|
+
yield request
|
|
156
|
+
|
|
157
|
+
client = httpx.Client(auth=APIKeyAuth("key"))
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Streaming Large Responses
|
|
161
|
+
```python
|
|
162
|
+
# Download large file
|
|
163
|
+
with client.stream("GET", url) as response:
|
|
164
|
+
response.raise_for_status()
|
|
165
|
+
with Path("large_file").open("wb") as f:
|
|
166
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
167
|
+
f.write(chunk)
|
|
168
|
+
|
|
169
|
+
# Async streaming
|
|
170
|
+
async with client.stream("GET", url) as response:
|
|
171
|
+
async for chunk in response.aiter_bytes():
|
|
172
|
+
process(chunk)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Testing
|
|
176
|
+
```python
|
|
177
|
+
import pytest
|
|
178
|
+
import respx
|
|
179
|
+
import httpx
|
|
180
|
+
|
|
181
|
+
@respx.mock
|
|
182
|
+
async def test_api():
|
|
183
|
+
respx.get("https://api.example.com/users").mock(
|
|
184
|
+
return_value=httpx.Response(200, json=[{"id": 1}])
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
async with httpx.AsyncClient() as client:
|
|
188
|
+
resp = await client.get("https://api.example.com/users")
|
|
189
|
+
assert resp.json() == [{"id": 1}]
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Decision Rules
|
|
195
|
+
|
|
196
|
+
| Situation | Pattern |
|
|
197
|
+
|-----------|---------|
|
|
198
|
+
| Simple sync requests | `httpx.Client()` |
|
|
199
|
+
| High concurrency sync | `httpx.Client` with limits |
|
|
200
|
+
| Async application | `httpx.AsyncClient` |
|
|
201
|
+
| Complex retry/backoff | `tenacity` + `httpx` |
|
|
202
|
+
| WebSocket | `httpx` (experimental) or `websockets` |
|
|
203
|
+
| Testing | `respx` (async) or `requests-mock` (sync) |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Preferred Patterns
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
# Reusable client factory
|
|
211
|
+
def create_client(
|
|
212
|
+
base_url: str,
|
|
213
|
+
timeout: float = 30.0,
|
|
214
|
+
api_key: str | None = None,
|
|
215
|
+
) -> httpx.Client:
|
|
216
|
+
headers = {"User-Agent": "MyApp/1.0"}
|
|
217
|
+
if api_key:
|
|
218
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
219
|
+
|
|
220
|
+
return httpx.Client(
|
|
221
|
+
base_url=base_url,
|
|
222
|
+
headers=headers,
|
|
223
|
+
timeout=httpx.Timeout(timeout),
|
|
224
|
+
limits=httpx.Limits(max_connections=50),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Service wrapper
|
|
228
|
+
class APIService:
|
|
229
|
+
def __init__(self, client: httpx.Client):
|
|
230
|
+
self.client = client
|
|
231
|
+
|
|
232
|
+
def get_user(self, user_id: str) -> User:
|
|
233
|
+
resp = self.client.get(f"/users/{user_id}")
|
|
234
|
+
resp.raise_for_status()
|
|
235
|
+
return User.model_validate(resp.json())
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Avoid
|
|
241
|
+
|
|
242
|
+
- Creating new client per request (no connection pooling)
|
|
243
|
+
- No timeout (hangs forever)
|
|
244
|
+
- Ignoring `raise_for_status()`
|
|
245
|
+
- Blocking sync client in async code
|
|
246
|
+
- Logging full response bodies (may contain secrets)
|
|
247
|
+
- Hardcoding URLs (use config + base_url)
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Validation Considerations
|
|
252
|
+
|
|
253
|
+
- Test timeout behavior
|
|
254
|
+
- Test retry logic
|
|
255
|
+
- Test auth handling
|
|
256
|
+
- Mock external APIs in tests
|
|
257
|
+
- Check connection pooling works
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Related Skills
|
|
262
|
+
|
|
263
|
+
- `generation/async_concurrency.md`
|
|
264
|
+
- `generation/error_handling.md`
|
|
265
|
+
- `engineering/configuration.md`
|
|
266
|
+
- `security/secrets.md`
|
|
267
|
+
- `testing/organization.md`
|