@malamute/ai-rules 1.0.0 → 1.2.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/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.cs"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LINQ Best Practices
|
|
7
|
+
|
|
8
|
+
## Method vs Query Syntax
|
|
9
|
+
|
|
10
|
+
```csharp
|
|
11
|
+
// Method syntax - preferred for simple queries
|
|
12
|
+
var activeUsers = users
|
|
13
|
+
.Where(u => u.IsActive)
|
|
14
|
+
.OrderBy(u => u.Name)
|
|
15
|
+
.ToList();
|
|
16
|
+
|
|
17
|
+
// Query syntax - better for joins and complex queries
|
|
18
|
+
var orderDetails = from order in orders
|
|
19
|
+
join customer in customers
|
|
20
|
+
on order.CustomerId equals customer.Id
|
|
21
|
+
join product in products
|
|
22
|
+
on order.ProductId equals product.Id
|
|
23
|
+
where order.Status == OrderStatus.Completed
|
|
24
|
+
select new
|
|
25
|
+
{
|
|
26
|
+
OrderId = order.Id,
|
|
27
|
+
CustomerName = customer.Name,
|
|
28
|
+
ProductName = product.Name
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Deferred vs Immediate Execution
|
|
33
|
+
|
|
34
|
+
```csharp
|
|
35
|
+
// Deferred - query not executed until enumerated
|
|
36
|
+
var query = users.Where(u => u.IsActive); // No DB call yet
|
|
37
|
+
|
|
38
|
+
// Immediate - forces execution
|
|
39
|
+
var list = users.Where(u => u.IsActive).ToList(); // Executes now
|
|
40
|
+
var array = users.Where(u => u.IsActive).ToArray(); // Executes now
|
|
41
|
+
var first = users.First(u => u.IsActive); // Executes now
|
|
42
|
+
var count = users.Count(u => u.IsActive); // Executes now
|
|
43
|
+
|
|
44
|
+
// BAD - multiple enumeration
|
|
45
|
+
IEnumerable<User> users = GetUsers();
|
|
46
|
+
if (users.Any()) // First enumeration
|
|
47
|
+
{
|
|
48
|
+
var first = users.First(); // Second enumeration - might get different data!
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// GOOD - materialize once
|
|
52
|
+
var users = GetUsers().ToList();
|
|
53
|
+
if (users.Count > 0)
|
|
54
|
+
{
|
|
55
|
+
var first = users[0];
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Filtering
|
|
60
|
+
|
|
61
|
+
```csharp
|
|
62
|
+
// Chain Where for readability
|
|
63
|
+
var results = items
|
|
64
|
+
.Where(x => x.IsActive)
|
|
65
|
+
.Where(x => x.Category == "Electronics")
|
|
66
|
+
.Where(x => x.Price > 100);
|
|
67
|
+
|
|
68
|
+
// Use Any/All for existence checks
|
|
69
|
+
if (users.Any(u => u.Role == Role.Admin)) // GOOD
|
|
70
|
+
if (users.Where(u => u.Role == Role.Admin).Count() > 0) // BAD
|
|
71
|
+
|
|
72
|
+
// FirstOrDefault with predicate
|
|
73
|
+
var admin = users.FirstOrDefault(u => u.Role == Role.Admin);
|
|
74
|
+
|
|
75
|
+
// SingleOrDefault when expecting 0 or 1
|
|
76
|
+
var user = users.SingleOrDefault(u => u.Email == email);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Projection
|
|
80
|
+
|
|
81
|
+
```csharp
|
|
82
|
+
// Select for transformation
|
|
83
|
+
var dtos = users.Select(u => new UserDto(u.Id, u.Name));
|
|
84
|
+
|
|
85
|
+
// SelectMany to flatten
|
|
86
|
+
var allOrders = customers.SelectMany(c => c.Orders);
|
|
87
|
+
|
|
88
|
+
// Anonymous types for intermediate results
|
|
89
|
+
var intermediate = users
|
|
90
|
+
.Select(u => new { u.Id, FullName = $"{u.FirstName} {u.LastName}" })
|
|
91
|
+
.Where(x => x.FullName.StartsWith("A"));
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Grouping
|
|
95
|
+
|
|
96
|
+
```csharp
|
|
97
|
+
// GroupBy
|
|
98
|
+
var usersByRole = users
|
|
99
|
+
.GroupBy(u => u.Role)
|
|
100
|
+
.Select(g => new
|
|
101
|
+
{
|
|
102
|
+
Role = g.Key,
|
|
103
|
+
Count = g.Count(),
|
|
104
|
+
Users = g.ToList()
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ToLookup - immediate execution, allows multiple enumeration
|
|
108
|
+
var lookup = users.ToLookup(u => u.Role);
|
|
109
|
+
var admins = lookup[Role.Admin];
|
|
110
|
+
var managers = lookup[Role.Manager];
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Aggregation
|
|
114
|
+
|
|
115
|
+
```csharp
|
|
116
|
+
// Aggregate functions
|
|
117
|
+
var total = orders.Sum(o => o.Amount);
|
|
118
|
+
var average = orders.Average(o => o.Amount);
|
|
119
|
+
var max = orders.Max(o => o.Amount);
|
|
120
|
+
var min = orders.Min(o => o.Amount);
|
|
121
|
+
|
|
122
|
+
// Aggregate for custom accumulation
|
|
123
|
+
var concatenated = names.Aggregate((a, b) => $"{a}, {b}");
|
|
124
|
+
|
|
125
|
+
// With seed value
|
|
126
|
+
var total = items.Aggregate(
|
|
127
|
+
seed: 0m,
|
|
128
|
+
func: (sum, item) => sum + item.Price * item.Quantity);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Set Operations
|
|
132
|
+
|
|
133
|
+
```csharp
|
|
134
|
+
// Distinct
|
|
135
|
+
var uniqueCategories = products.Select(p => p.Category).Distinct();
|
|
136
|
+
|
|
137
|
+
// DistinctBy (C# 10+)
|
|
138
|
+
var uniqueByName = products.DistinctBy(p => p.Name);
|
|
139
|
+
|
|
140
|
+
// Union, Intersect, Except
|
|
141
|
+
var allIds = list1.Select(x => x.Id).Union(list2.Select(x => x.Id));
|
|
142
|
+
var commonIds = list1.Select(x => x.Id).Intersect(list2.Select(x => x.Id));
|
|
143
|
+
var onlyInFirst = list1.Select(x => x.Id).Except(list2.Select(x => x.Id));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Ordering
|
|
147
|
+
|
|
148
|
+
```csharp
|
|
149
|
+
// Multiple sort criteria
|
|
150
|
+
var sorted = users
|
|
151
|
+
.OrderBy(u => u.LastName)
|
|
152
|
+
.ThenBy(u => u.FirstName)
|
|
153
|
+
.ThenByDescending(u => u.CreatedAt);
|
|
154
|
+
|
|
155
|
+
// Order vs OrderBy (C# 11+)
|
|
156
|
+
var ordered = items.Order(); // Uses default comparer
|
|
157
|
+
var orderedDesc = items.OrderDescending();
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Pagination
|
|
161
|
+
|
|
162
|
+
```csharp
|
|
163
|
+
// Skip/Take for pagination
|
|
164
|
+
var page = users
|
|
165
|
+
.OrderBy(u => u.Id)
|
|
166
|
+
.Skip((pageNumber - 1) * pageSize)
|
|
167
|
+
.Take(pageSize)
|
|
168
|
+
.ToList();
|
|
169
|
+
|
|
170
|
+
// Chunk for batching (C# 10+)
|
|
171
|
+
var batches = items.Chunk(100);
|
|
172
|
+
foreach (var batch in batches)
|
|
173
|
+
{
|
|
174
|
+
await ProcessBatchAsync(batch);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## EF Core Specific
|
|
179
|
+
|
|
180
|
+
```csharp
|
|
181
|
+
// GOOD - let EF translate to SQL
|
|
182
|
+
var users = await _context.Users
|
|
183
|
+
.Where(u => u.IsActive)
|
|
184
|
+
.OrderBy(u => u.Name)
|
|
185
|
+
.ToListAsync();
|
|
186
|
+
|
|
187
|
+
// BAD - client-side evaluation (throws in EF Core 3+)
|
|
188
|
+
var users = await _context.Users
|
|
189
|
+
.Where(u => SomeLocalMethod(u)) // Can't translate
|
|
190
|
+
.ToListAsync();
|
|
191
|
+
|
|
192
|
+
// GOOD - explicit client evaluation when needed
|
|
193
|
+
var users = await _context.Users
|
|
194
|
+
.Where(u => u.IsActive)
|
|
195
|
+
.AsEnumerable() // Switch to client
|
|
196
|
+
.Where(u => SomeLocalMethod(u))
|
|
197
|
+
.ToList();
|
|
198
|
+
|
|
199
|
+
// Use AsNoTracking for read-only queries
|
|
200
|
+
var users = await _context.Users
|
|
201
|
+
.AsNoTracking()
|
|
202
|
+
.Where(u => u.IsActive)
|
|
203
|
+
.ToListAsync();
|
|
204
|
+
|
|
205
|
+
// Projection to avoid over-fetching
|
|
206
|
+
var dtos = await _context.Users
|
|
207
|
+
.Where(u => u.IsActive)
|
|
208
|
+
.Select(u => new UserDto(u.Id, u.Name, u.Email))
|
|
209
|
+
.ToListAsync();
|
|
210
|
+
```
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/handlers/**/*.py"
|
|
4
|
+
- "**/services/**/*.py"
|
|
5
|
+
- "**/consumers/**/*.py"
|
|
6
|
+
- "**/workers/**/*.py"
|
|
7
|
+
- "**/*_async*.py"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Python Async Patterns
|
|
11
|
+
|
|
12
|
+
## Async Function Basics
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import asyncio
|
|
16
|
+
from typing import AsyncIterator
|
|
17
|
+
|
|
18
|
+
# Always use async def for I/O operations
|
|
19
|
+
async def fetch_user(user_id: int) -> User:
|
|
20
|
+
return await db.users.get(user_id)
|
|
21
|
+
|
|
22
|
+
# Never mix sync I/O in async functions
|
|
23
|
+
# BAD
|
|
24
|
+
async def bad_fetch():
|
|
25
|
+
return requests.get(url) # Blocks event loop!
|
|
26
|
+
|
|
27
|
+
# GOOD
|
|
28
|
+
async def good_fetch():
|
|
29
|
+
async with httpx.AsyncClient() as client:
|
|
30
|
+
return await client.get(url)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Concurrent Execution
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
|
|
38
|
+
# Run tasks concurrently
|
|
39
|
+
async def fetch_all_data():
|
|
40
|
+
# Wrong - sequential execution
|
|
41
|
+
users = await fetch_users()
|
|
42
|
+
orders = await fetch_orders()
|
|
43
|
+
products = await fetch_products()
|
|
44
|
+
|
|
45
|
+
# Right - concurrent execution
|
|
46
|
+
users, orders, products = await asyncio.gather(
|
|
47
|
+
fetch_users(),
|
|
48
|
+
fetch_orders(),
|
|
49
|
+
fetch_products(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return users, orders, products
|
|
53
|
+
|
|
54
|
+
# With error handling
|
|
55
|
+
async def fetch_with_errors():
|
|
56
|
+
results = await asyncio.gather(
|
|
57
|
+
fetch_users(),
|
|
58
|
+
fetch_orders(),
|
|
59
|
+
fetch_products(),
|
|
60
|
+
return_exceptions=True, # Don't fail on first error
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for result in results:
|
|
64
|
+
if isinstance(result, Exception):
|
|
65
|
+
logger.error(f"Task failed: {result}")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## TaskGroup (Python 3.11+)
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
async def process_items(items: list[Item]) -> list[Result]:
|
|
72
|
+
results = []
|
|
73
|
+
|
|
74
|
+
async with asyncio.TaskGroup() as tg:
|
|
75
|
+
for item in items:
|
|
76
|
+
tg.create_task(process_item(item))
|
|
77
|
+
|
|
78
|
+
# All tasks complete when exiting context
|
|
79
|
+
return results
|
|
80
|
+
|
|
81
|
+
# With exception handling
|
|
82
|
+
async def process_with_handling(items: list[Item]):
|
|
83
|
+
try:
|
|
84
|
+
async with asyncio.TaskGroup() as tg:
|
|
85
|
+
for item in items:
|
|
86
|
+
tg.create_task(process_item(item))
|
|
87
|
+
except* ValueError as eg:
|
|
88
|
+
for exc in eg.exceptions:
|
|
89
|
+
logger.error(f"Validation error: {exc}")
|
|
90
|
+
except* ConnectionError as eg:
|
|
91
|
+
for exc in eg.exceptions:
|
|
92
|
+
logger.error(f"Connection error: {exc}")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Async Context Managers
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from contextlib import asynccontextmanager
|
|
99
|
+
|
|
100
|
+
@asynccontextmanager
|
|
101
|
+
async def get_db_connection():
|
|
102
|
+
conn = await create_connection()
|
|
103
|
+
try:
|
|
104
|
+
yield conn
|
|
105
|
+
finally:
|
|
106
|
+
await conn.close()
|
|
107
|
+
|
|
108
|
+
# Usage
|
|
109
|
+
async def query_users():
|
|
110
|
+
async with get_db_connection() as conn:
|
|
111
|
+
return await conn.fetch("SELECT * FROM users")
|
|
112
|
+
|
|
113
|
+
# Class-based context manager
|
|
114
|
+
class AsyncDatabaseSession:
|
|
115
|
+
async def __aenter__(self):
|
|
116
|
+
self.session = await create_session()
|
|
117
|
+
return self.session
|
|
118
|
+
|
|
119
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
120
|
+
if exc_type:
|
|
121
|
+
await self.session.rollback()
|
|
122
|
+
else:
|
|
123
|
+
await self.session.commit()
|
|
124
|
+
await self.session.close()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Async Iterators
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from typing import AsyncIterator
|
|
131
|
+
|
|
132
|
+
async def fetch_pages(url: str) -> AsyncIterator[Page]:
|
|
133
|
+
next_url = url
|
|
134
|
+
while next_url:
|
|
135
|
+
response = await fetch(next_url)
|
|
136
|
+
yield response.data
|
|
137
|
+
next_url = response.next_url
|
|
138
|
+
|
|
139
|
+
# Usage
|
|
140
|
+
async def process_all_pages():
|
|
141
|
+
async for page in fetch_pages("/api/items"):
|
|
142
|
+
for item in page.items:
|
|
143
|
+
await process_item(item)
|
|
144
|
+
|
|
145
|
+
# Async comprehension
|
|
146
|
+
async def get_all_items():
|
|
147
|
+
return [
|
|
148
|
+
item
|
|
149
|
+
async for page in fetch_pages("/api/items")
|
|
150
|
+
for item in page.items
|
|
151
|
+
]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Semaphore for Rate Limiting
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import asyncio
|
|
158
|
+
|
|
159
|
+
async def fetch_with_limit(urls: list[str], max_concurrent: int = 10):
|
|
160
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
161
|
+
|
|
162
|
+
async def fetch_one(url: str):
|
|
163
|
+
async with semaphore:
|
|
164
|
+
async with httpx.AsyncClient() as client:
|
|
165
|
+
return await client.get(url)
|
|
166
|
+
|
|
167
|
+
return await asyncio.gather(*[fetch_one(url) for url in urls])
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Timeouts
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import asyncio
|
|
174
|
+
|
|
175
|
+
async def fetch_with_timeout(url: str, timeout: float = 10.0):
|
|
176
|
+
try:
|
|
177
|
+
async with asyncio.timeout(timeout):
|
|
178
|
+
return await fetch(url)
|
|
179
|
+
except asyncio.TimeoutError:
|
|
180
|
+
logger.error(f"Timeout fetching {url}")
|
|
181
|
+
raise
|
|
182
|
+
|
|
183
|
+
# Or with wait_for (older style)
|
|
184
|
+
async def fetch_with_wait_for(url: str):
|
|
185
|
+
try:
|
|
186
|
+
return await asyncio.wait_for(fetch(url), timeout=10.0)
|
|
187
|
+
except asyncio.TimeoutError:
|
|
188
|
+
raise
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Background Tasks
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
import asyncio
|
|
195
|
+
from collections.abc import Callable
|
|
196
|
+
|
|
197
|
+
class BackgroundTasks:
|
|
198
|
+
def __init__(self):
|
|
199
|
+
self._tasks: set[asyncio.Task] = set()
|
|
200
|
+
|
|
201
|
+
def add_task(self, coro):
|
|
202
|
+
task = asyncio.create_task(coro)
|
|
203
|
+
self._tasks.add(task)
|
|
204
|
+
task.add_done_callback(self._tasks.discard)
|
|
205
|
+
|
|
206
|
+
async def shutdown(self):
|
|
207
|
+
for task in self._tasks:
|
|
208
|
+
task.cancel()
|
|
209
|
+
await asyncio.gather(*self._tasks, return_exceptions=True)
|
|
210
|
+
|
|
211
|
+
# Usage in FastAPI
|
|
212
|
+
background = BackgroundTasks()
|
|
213
|
+
|
|
214
|
+
@app.post("/orders")
|
|
215
|
+
async def create_order(order: Order):
|
|
216
|
+
saved = await save_order(order)
|
|
217
|
+
background.add_task(send_notification(order))
|
|
218
|
+
return saved
|
|
219
|
+
|
|
220
|
+
@app.on_event("shutdown")
|
|
221
|
+
async def shutdown():
|
|
222
|
+
await background.shutdown()
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Async Queue
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
import asyncio
|
|
229
|
+
|
|
230
|
+
async def producer(queue: asyncio.Queue[int]):
|
|
231
|
+
for i in range(10):
|
|
232
|
+
await queue.put(i)
|
|
233
|
+
print(f"Produced: {i}")
|
|
234
|
+
await asyncio.sleep(0.1)
|
|
235
|
+
await queue.put(None) # Sentinel to stop
|
|
236
|
+
|
|
237
|
+
async def consumer(queue: asyncio.Queue[int]):
|
|
238
|
+
while True:
|
|
239
|
+
item = await queue.get()
|
|
240
|
+
if item is None:
|
|
241
|
+
break
|
|
242
|
+
print(f"Consumed: {item}")
|
|
243
|
+
queue.task_done()
|
|
244
|
+
|
|
245
|
+
async def main():
|
|
246
|
+
queue: asyncio.Queue[int] = asyncio.Queue(maxsize=5)
|
|
247
|
+
|
|
248
|
+
await asyncio.gather(
|
|
249
|
+
producer(queue),
|
|
250
|
+
consumer(queue),
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Async Lock
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
import asyncio
|
|
258
|
+
|
|
259
|
+
class Counter:
|
|
260
|
+
def __init__(self):
|
|
261
|
+
self._value = 0
|
|
262
|
+
self._lock = asyncio.Lock()
|
|
263
|
+
|
|
264
|
+
async def increment(self):
|
|
265
|
+
async with self._lock:
|
|
266
|
+
self._value += 1
|
|
267
|
+
return self._value
|
|
268
|
+
|
|
269
|
+
async def get(self) -> int:
|
|
270
|
+
async with self._lock:
|
|
271
|
+
return self._value
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## SQLAlchemy Async
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
278
|
+
from sqlalchemy.orm import sessionmaker
|
|
279
|
+
|
|
280
|
+
# Create async engine
|
|
281
|
+
engine = create_async_engine(
|
|
282
|
+
"postgresql+asyncpg://user:pass@localhost/db",
|
|
283
|
+
echo=True,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Async session factory
|
|
287
|
+
async_session = sessionmaker(
|
|
288
|
+
engine,
|
|
289
|
+
class_=AsyncSession,
|
|
290
|
+
expire_on_commit=False,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Usage
|
|
294
|
+
async def get_user(user_id: int) -> User | None:
|
|
295
|
+
async with async_session() as session:
|
|
296
|
+
result = await session.execute(
|
|
297
|
+
select(User).where(User.id == user_id)
|
|
298
|
+
)
|
|
299
|
+
return result.scalar_one_or_none()
|
|
300
|
+
|
|
301
|
+
# FastAPI dependency
|
|
302
|
+
async def get_db() -> AsyncIterator[AsyncSession]:
|
|
303
|
+
async with async_session() as session:
|
|
304
|
+
try:
|
|
305
|
+
yield session
|
|
306
|
+
await session.commit()
|
|
307
|
+
except Exception:
|
|
308
|
+
await session.rollback()
|
|
309
|
+
raise
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## httpx Async Client
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
import httpx
|
|
316
|
+
|
|
317
|
+
# Reuse client for connection pooling
|
|
318
|
+
class ApiClient:
|
|
319
|
+
def __init__(self, base_url: str):
|
|
320
|
+
self._client = httpx.AsyncClient(
|
|
321
|
+
base_url=base_url,
|
|
322
|
+
timeout=30.0,
|
|
323
|
+
headers={"User-Agent": "MyApp/1.0"},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
async def get(self, path: str) -> dict:
|
|
327
|
+
response = await self._client.get(path)
|
|
328
|
+
response.raise_for_status()
|
|
329
|
+
return response.json()
|
|
330
|
+
|
|
331
|
+
async def close(self):
|
|
332
|
+
await self._client.aclose()
|
|
333
|
+
|
|
334
|
+
# Usage with context manager
|
|
335
|
+
async with httpx.AsyncClient() as client:
|
|
336
|
+
response = await client.get("https://api.example.com/data")
|
|
337
|
+
```
|