cc-codeconductor 0.2.1
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/LICENSE +18 -0
- package/README.md +319 -0
- package/dist/index.js +12741 -0
- package/package.json +51 -0
- package/policy.yml +124 -0
- package/presets/claude/CLAUDE.md +598 -0
- package/presets/claude/commands/cc/feature.md +115 -0
- package/presets/claude/commands/cc/fix.md +121 -0
- package/presets/claude/commands/cc/refactor.md +148 -0
- package/presets/claude/commands/cc/review.md +126 -0
- package/presets/claude/commands/cc/tdd-cycle.md +226 -0
- package/presets/claude/commands/cc/test-plan.md +138 -0
- package/presets/claude/settings.json +37 -0
- package/presets/claude/skills/api-versioning/SKILL.md +389 -0
- package/presets/claude/skills/django-orm/SKILL.md +455 -0
- package/presets/claude/skills/django-testing/SKILL.md +409 -0
- package/presets/claude/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/claude/skills/python/SKILL.md +606 -0
- package/presets/claude/skills/python-django-stack/SKILL.md +492 -0
- package/presets/claude/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/claude/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/claude/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/claude/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/claude/skills/testing-strategy/SKILL.md +479 -0
- package/presets/codex/AGENTS.md +883 -0
- package/presets/codex/README.md +102 -0
- package/presets/codex/skills/api-versioning/SKILL.md +389 -0
- package/presets/codex/skills/django-orm/SKILL.md +455 -0
- package/presets/codex/skills/django-testing/SKILL.md +409 -0
- package/presets/codex/skills/jpa-postgres/SKILL.md +618 -0
- package/presets/codex/skills/python/SKILL.md +606 -0
- package/presets/codex/skills/python-django-stack/SKILL.md +492 -0
- package/presets/codex/skills/python-fastapi-stack/SKILL.md +457 -0
- package/presets/codex/skills/spring-boot-feature/SKILL.md +558 -0
- package/presets/codex/skills/spring-boot-kotlin/SKILL.md +400 -0
- package/presets/codex/skills/sqlalchemy/SKILL.md +466 -0
- package/presets/codex/skills/testing-strategy/SKILL.md +479 -0
- package/presets/opencode/README.md +191 -0
- package/presets/opencode/agents/architect.md +133 -0
- package/presets/opencode/agents/docs.md +113 -0
- package/presets/opencode/agents/implementer.md +116 -0
- package/presets/opencode/agents/orchestrator.md +248 -0
- package/presets/opencode/agents/repo-explorer.md +119 -0
- package/presets/opencode/agents/reviewer.md +130 -0
- package/presets/opencode/agents/task-coach.md +103 -0
- package/presets/opencode/agents/tester.md +269 -0
- package/presets/opencode/commands/cc-feature.md +114 -0
- package/presets/opencode/commands/cc-fix.md +123 -0
- package/presets/opencode/commands/cc-refactor.md +148 -0
- package/presets/opencode/commands/cc-review.md +141 -0
- package/presets/opencode/commands/cc-tdd-cycle.md +225 -0
- package/presets/opencode/commands/cc-test-plan.md +144 -0
- package/presets/opencode/opencode.jsonc +89 -0
- package/presets/opencode/prompts/v0.1.0/architect.md +213 -0
- package/presets/opencode/prompts/v0.1.0/docs.md +181 -0
- package/presets/opencode/prompts/v0.1.0/implementer.md +154 -0
- package/presets/opencode/prompts/v0.1.0/orchestrator.md +169 -0
- package/presets/opencode/prompts/v0.1.0/repo-explorer.md +102 -0
- package/presets/opencode/prompts/v0.1.0/reviewer.md +183 -0
- package/presets/opencode/prompts/v0.1.0/task-coach.md +142 -0
- package/presets/opencode/prompts/v0.1.0/tester.md +160 -0
- package/presets/opencode/prompts/v0.2.0/architect.md +219 -0
- package/presets/opencode/prompts/v0.2.0/docs.md +187 -0
- package/presets/opencode/prompts/v0.2.0/implementer.md +160 -0
- package/presets/opencode/prompts/v0.2.0/orchestrator.md +238 -0
- package/presets/opencode/prompts/v0.2.0/repo-explorer.md +108 -0
- package/presets/opencode/prompts/v0.2.0/reviewer.md +190 -0
- package/presets/opencode/prompts/v0.2.0/task-coach.md +153 -0
- package/presets/opencode/prompts/v0.2.0/tester.md +249 -0
- package/presets/opencode/skills/api-versioning/SKILL.md +388 -0
- package/presets/opencode/skills/astro/SKILL.md +313 -0
- package/presets/opencode/skills/code-review/SKILL.md +202 -0
- package/presets/opencode/skills/django-orm/SKILL.md +455 -0
- package/presets/opencode/skills/django-testing/SKILL.md +408 -0
- package/presets/opencode/skills/django-uv/SKILL.md +400 -0
- package/presets/opencode/skills/jpa-postgres/SKILL.md +617 -0
- package/presets/opencode/skills/nextjs-typescript/SKILL.md +385 -0
- package/presets/opencode/skills/python/SKILL.md +605 -0
- package/presets/opencode/skills/python-django-stack/SKILL.md +491 -0
- package/presets/opencode/skills/python-fastapi-stack/SKILL.md +456 -0
- package/presets/opencode/skills/security/SKILL.md +376 -0
- package/presets/opencode/skills/spring-boot-feature/SKILL.md +557 -0
- package/presets/opencode/skills/spring-boot-kotlin/SKILL.md +399 -0
- package/presets/opencode/skills/spring-boot-testing-strategy/SKILL.md +470 -0
- package/presets/opencode/skills/sqlalchemy/SKILL.md +465 -0
- package/presets/opencode/skills/testing-tdd/SKILL.md +586 -0
- package/src/presets/council/council.yml +59 -0
- package/src/presets/manifests/claude.yml +23 -0
- package/src/presets/manifests/codex.yml +12 -0
- package/src/presets/manifests/opencode.yml +18 -0
- package/src/presets/models/claude.yml +37 -0
- package/src/presets/models/codex.yml +38 -0
- package/src/presets/models/opencode.yml +37 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: python
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
name: Python
|
|
5
|
+
description: >
|
|
6
|
+
Python development best practices: clean code, patterns,
|
|
7
|
+
type hints, decorators, context managers, and architecture.
|
|
8
|
+
Trigger: When writing Python code anywhere in the project.
|
|
9
|
+
|
|
10
|
+
compatibility:
|
|
11
|
+
tools: [claude, codex, opencode]
|
|
12
|
+
stacks:
|
|
13
|
+
languages: [python]
|
|
14
|
+
frameworks: [django, fastapi, flask]
|
|
15
|
+
|
|
16
|
+
risk:
|
|
17
|
+
level: low
|
|
18
|
+
can_execute_shell: false
|
|
19
|
+
can_modify_files: true
|
|
20
|
+
requires_network: false
|
|
21
|
+
|
|
22
|
+
inputs:
|
|
23
|
+
- Python source files
|
|
24
|
+
- existing functions, classes, or modules
|
|
25
|
+
- business logic implementations to review or refactor
|
|
26
|
+
|
|
27
|
+
outputs:
|
|
28
|
+
- clean Python functions and classes with meaningful names
|
|
29
|
+
- type-annotated public API signatures
|
|
30
|
+
- custom domain exceptions
|
|
31
|
+
- value objects and data classes
|
|
32
|
+
- service and repository pattern implementations
|
|
33
|
+
|
|
34
|
+
quality:
|
|
35
|
+
reviewed_by: codeconductor-core
|
|
36
|
+
version: 0.1.0
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## When to Use
|
|
40
|
+
|
|
41
|
+
- Writing any Python code in the project
|
|
42
|
+
- Reviewing code for quality and maintainability
|
|
43
|
+
- Designing new functions, classes, or modules
|
|
44
|
+
- Implementing business logic or utilities
|
|
45
|
+
|
|
46
|
+
## Clean Code Principles
|
|
47
|
+
|
|
48
|
+
### Readability > Brevity
|
|
49
|
+
|
|
50
|
+
Code is read more times than it's written. Prioritize clarity over cleverness:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# WRONG — clever but obscure
|
|
54
|
+
def f(x): return x if x else 0
|
|
55
|
+
|
|
56
|
+
# CORRECT — readable
|
|
57
|
+
def calculate_discount(price, has_discount):
|
|
58
|
+
if not has_discount:
|
|
59
|
+
return 0
|
|
60
|
+
return price * DISCOUNT_RATE
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Meaningful Names
|
|
64
|
+
|
|
65
|
+
| What | Convention | Example |
|
|
66
|
+
| --------- | ---------------------- | -------------------------------------------- |
|
|
67
|
+
| Variables | descriptive snake_case | `total_price`, `products_list` |
|
|
68
|
+
| Functions | verb snake_case | `get_active_products()`, `calculate_total()` |
|
|
69
|
+
| Classes | PascalCase | `OrderService`, `CartController` |
|
|
70
|
+
| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT`, `DEFAULT_PAGE_SIZE` |
|
|
71
|
+
| Modules | snake_case | `order_service.py`, `cart_utils.py` |
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# WRONG — cryptic names
|
|
75
|
+
d = 1500
|
|
76
|
+
p = products.filter(a=True)
|
|
77
|
+
|
|
78
|
+
# CORRECT — names that say what they are
|
|
79
|
+
discount_amount = 1500
|
|
80
|
+
active_products = Product.objects.filter(is_active=True)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Small Functions
|
|
84
|
+
|
|
85
|
+
A function should do ONE thing. Ideally under 30 lines:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# WRONG — function does many things
|
|
89
|
+
def process_order(order_data):
|
|
90
|
+
# validates
|
|
91
|
+
# creates order
|
|
92
|
+
# creates items
|
|
93
|
+
# updates stock
|
|
94
|
+
# sends email
|
|
95
|
+
# updates analytics
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
# CORRECT — small, focused functions
|
|
99
|
+
def process_order(order_data):
|
|
100
|
+
validated = validate_order_data(order_data)
|
|
101
|
+
order = create_order(validated)
|
|
102
|
+
create_order_items(order, validated["items"])
|
|
103
|
+
update_product_stock(validated["items"])
|
|
104
|
+
send_order_confirmation(order)
|
|
105
|
+
track_order_analytics(order)
|
|
106
|
+
return order
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### DRY — Don't Repeat Yourself
|
|
110
|
+
|
|
111
|
+
Extract repeated code into functions or classes:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# WRONG — repeat logic
|
|
115
|
+
if order.status == "pending":
|
|
116
|
+
send_email(order.customer, "Your order is pending")
|
|
117
|
+
log_event("order_pending", order.id)
|
|
118
|
+
|
|
119
|
+
if order.status == "paid":
|
|
120
|
+
send_email(order.customer, "Your order was paid")
|
|
121
|
+
log_event("order_paid", order.id)
|
|
122
|
+
|
|
123
|
+
# CORRECT — extract to function
|
|
124
|
+
def notify_order_status(order):
|
|
125
|
+
status_messages = {
|
|
126
|
+
"pending": "Your order is pending",
|
|
127
|
+
"paid": "Your order was paid",
|
|
128
|
+
}
|
|
129
|
+
send_email(order.customer, status_messages[order.status])
|
|
130
|
+
log_event(f"order_{order.status}", order.id)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Type Hints
|
|
134
|
+
|
|
135
|
+
### When to Use Them
|
|
136
|
+
|
|
137
|
+
Use type hints on:
|
|
138
|
+
|
|
139
|
+
- Public function signatures (APIs, services)
|
|
140
|
+
- Functions with complex parameters
|
|
141
|
+
- Non-obvious returns
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from typing import Optional, List, Dict, Any
|
|
145
|
+
|
|
146
|
+
# Public functions — use hints
|
|
147
|
+
def calculate_total(items: List[Dict[str, Any]], tax_rate: float) -> float:
|
|
148
|
+
...
|
|
149
|
+
|
|
150
|
+
def find_product(product_id: int) -> Optional[Product]:
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
# Private helpers — optional
|
|
154
|
+
def _build_cart_response(cart):
|
|
155
|
+
...
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Basic Types
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
# Primitives
|
|
162
|
+
name: str = "Product"
|
|
163
|
+
quantity: int = 5
|
|
164
|
+
price: float = 1500.99
|
|
165
|
+
is_active: bool = True
|
|
166
|
+
|
|
167
|
+
# Collections
|
|
168
|
+
products: List[Product] = []
|
|
169
|
+
product_ids: list[int] = []
|
|
170
|
+
metadata: Dict[str, Any] = {}
|
|
171
|
+
items: tuple[str, int] = ("sku", 5)
|
|
172
|
+
|
|
173
|
+
# Optionals
|
|
174
|
+
user: Optional[User] = None
|
|
175
|
+
description: str | None = None
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Advanced Types
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from typing import Union, Optional, Callable, Any
|
|
182
|
+
from decimal import Decimal
|
|
183
|
+
|
|
184
|
+
# Union for multiple types
|
|
185
|
+
def process_payment(amount: float, method: str) -> Union[dict, None]:
|
|
186
|
+
if method == "cash":
|
|
187
|
+
return {"change": amount - 1000}
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
# Callable for functions as parameters
|
|
191
|
+
def execute_callback(callback: Callable[[str], None], message: str):
|
|
192
|
+
callback(message)
|
|
193
|
+
|
|
194
|
+
# TypeAlias for complex types
|
|
195
|
+
CartData = Dict[str, Dict[str, Union[int, str]]]
|
|
196
|
+
|
|
197
|
+
def process_cart(cart: CartData):
|
|
198
|
+
...
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Common Patterns
|
|
202
|
+
|
|
203
|
+
### Context Managers
|
|
204
|
+
|
|
205
|
+
For resources needing cleanup (files, connections, transactions):
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
# WRONG — no guaranteed cleanup
|
|
209
|
+
def write_report(data):
|
|
210
|
+
f = open("report.txt", "w")
|
|
211
|
+
f.write(data)
|
|
212
|
+
f.close() # Won't run if exception occurs
|
|
213
|
+
|
|
214
|
+
# CORRECT — context manager
|
|
215
|
+
def write_report(data):
|
|
216
|
+
with open("report.txt", "w") as f:
|
|
217
|
+
f.write(data)
|
|
218
|
+
# Automatic cleanup
|
|
219
|
+
|
|
220
|
+
# Custom context manager
|
|
221
|
+
class Transaction:
|
|
222
|
+
def __init__(self):
|
|
223
|
+
self.entered = False
|
|
224
|
+
|
|
225
|
+
def __enter__(self):
|
|
226
|
+
db.begin()
|
|
227
|
+
self.entered = True
|
|
228
|
+
return self
|
|
229
|
+
|
|
230
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
231
|
+
if exc_type:
|
|
232
|
+
db.rollback()
|
|
233
|
+
else:
|
|
234
|
+
db.commit()
|
|
235
|
+
return False # Don't suppress exceptions
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Decorators
|
|
239
|
+
|
|
240
|
+
Functions that modify the behavior of other functions:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
import functools
|
|
244
|
+
import time
|
|
245
|
+
|
|
246
|
+
# Basic decorator
|
|
247
|
+
def require_auth(view):
|
|
248
|
+
@functools.wraps(view)
|
|
249
|
+
def wrapper(request, *args, **kwargs):
|
|
250
|
+
if not request.user.is_authenticated:
|
|
251
|
+
return JsonResponse({"error": "Unauthorized"}, status=401)
|
|
252
|
+
return view(request, *args, **kwargs)
|
|
253
|
+
return wrapper
|
|
254
|
+
|
|
255
|
+
# Decorator with parameters
|
|
256
|
+
def require_role(role: str):
|
|
257
|
+
def decorator(view):
|
|
258
|
+
@functools.wraps(view)
|
|
259
|
+
def wrapper(request, *args, **kwargs):
|
|
260
|
+
if request.user.role != role:
|
|
261
|
+
return JsonResponse({"error": "Forbidden"}, status=403)
|
|
262
|
+
return view(request, *args, **kwargs)
|
|
263
|
+
return wrapper
|
|
264
|
+
return decorator
|
|
265
|
+
|
|
266
|
+
# Decorator with timing
|
|
267
|
+
def timing(view):
|
|
268
|
+
@functools.wraps(view)
|
|
269
|
+
def wrapper(*args, **kwargs):
|
|
270
|
+
start = time.perf_counter()
|
|
271
|
+
result = view(*args, **kwargs)
|
|
272
|
+
elapsed = time.perf_counter() - start
|
|
273
|
+
print(f"{view.__name__} took {elapsed:.4f}s")
|
|
274
|
+
return result
|
|
275
|
+
return wrapper
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Data Classes
|
|
279
|
+
|
|
280
|
+
For simple data structures:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from dataclasses import dataclass
|
|
284
|
+
from datetime import datetime
|
|
285
|
+
from decimal import Decimal
|
|
286
|
+
|
|
287
|
+
@dataclass
|
|
288
|
+
class CartItem:
|
|
289
|
+
product_id: int
|
|
290
|
+
quantity: int
|
|
291
|
+
price: Decimal
|
|
292
|
+
name: str = ""
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def subtotal(self) -> Decimal:
|
|
296
|
+
return self.price * self.quantity
|
|
297
|
+
|
|
298
|
+
# Usage
|
|
299
|
+
item = CartItem(product_id=1, quantity=2, price=Decimal("1500.00"))
|
|
300
|
+
print(item.subtotal) # 3000.00
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Enums
|
|
304
|
+
|
|
305
|
+
For limited values:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from enum import Enum
|
|
309
|
+
|
|
310
|
+
class OrderStatus(str, Enum):
|
|
311
|
+
PENDING = "pending"
|
|
312
|
+
PAID = "paid"
|
|
313
|
+
SHIPPED = "shipped"
|
|
314
|
+
CANCELLED = "cancelled"
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def is_final(self):
|
|
318
|
+
return self in (OrderStatus.SHIPPED, OrderStatus.CANCELLED)
|
|
319
|
+
|
|
320
|
+
# Usage
|
|
321
|
+
order.status = OrderStatus.PAID
|
|
322
|
+
if order.status.is_final:
|
|
323
|
+
...
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Errors and Exceptions
|
|
327
|
+
|
|
328
|
+
### Exception Handling
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
# WRONG — catch generic Exception
|
|
332
|
+
try:
|
|
333
|
+
product = Product.objects.get(id=product_id)
|
|
334
|
+
except Exception:
|
|
335
|
+
return JsonResponse({"error": "Not found"}, status=404)
|
|
336
|
+
|
|
337
|
+
# CORRECT — catch specific exception
|
|
338
|
+
try:
|
|
339
|
+
product = Product.objects.get(id=product_id)
|
|
340
|
+
except Product.DoesNotExist:
|
|
341
|
+
return JsonResponse({"error": "Product not found"}, status=404)
|
|
342
|
+
|
|
343
|
+
# With fallback
|
|
344
|
+
product = Product.objects.filter(id=product_id).first()
|
|
345
|
+
if not product:
|
|
346
|
+
return JsonResponse({"error": "Product not found"}, status=404)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Custom Exceptions
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
class BusinessException(Exception):
|
|
353
|
+
"""Exception for business logic errors."""
|
|
354
|
+
def __init__(self, message: str, code: str = "BUSINESS_ERROR"):
|
|
355
|
+
self.message = message
|
|
356
|
+
self.code = code
|
|
357
|
+
super().__init__(self.message)
|
|
358
|
+
|
|
359
|
+
class InsufficientStockException(BusinessException):
|
|
360
|
+
def __init__(self, product_id: int, requested: int, available: int):
|
|
361
|
+
super().__init__(
|
|
362
|
+
message=f"Insufficient stock: requested {requested}, available {available}",
|
|
363
|
+
code="INSUFFICIENT_STOCK",
|
|
364
|
+
)
|
|
365
|
+
self.product_id = product_id
|
|
366
|
+
self.requested = requested
|
|
367
|
+
self.available = available
|
|
368
|
+
|
|
369
|
+
# Usage
|
|
370
|
+
try:
|
|
371
|
+
if requested > available:
|
|
372
|
+
raise InsufficientStockException(product_id, requested, available)
|
|
373
|
+
except InsufficientStockException as e:
|
|
374
|
+
return JsonResponse({"error": e.message, "code": e.code}, status=400)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## Architecture Patterns
|
|
378
|
+
|
|
379
|
+
### Repository Pattern
|
|
380
|
+
|
|
381
|
+
Abstract data access:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
class ProductRepository:
|
|
385
|
+
@staticmethod
|
|
386
|
+
def get_active() -> QuerySet:
|
|
387
|
+
return Product.objects.filter(is_active=True)
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def get_by_category(category_id: int) -> QuerySet:
|
|
391
|
+
return Product.objects.filter(
|
|
392
|
+
category_id=category_id,
|
|
393
|
+
is_active=True,
|
|
394
|
+
).select_related("category")
|
|
395
|
+
|
|
396
|
+
@staticmethod
|
|
397
|
+
def search(query: str) -> QuerySet:
|
|
398
|
+
return Product.objects.filter(
|
|
399
|
+
Q(name__icontains=query) | Q(sku__icontains=query),
|
|
400
|
+
is_active=True,
|
|
401
|
+
)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Service Layer
|
|
405
|
+
|
|
406
|
+
Encapsulated business logic:
|
|
407
|
+
|
|
408
|
+
```python
|
|
409
|
+
class ProductService:
|
|
410
|
+
@staticmethod
|
|
411
|
+
def get_products_for_pos(store: Store) -> List[Dict]:
|
|
412
|
+
products = ProductRepository.get_active().select_related(
|
|
413
|
+
"category", "tax"
|
|
414
|
+
)
|
|
415
|
+
return [
|
|
416
|
+
{
|
|
417
|
+
"id": p.id,
|
|
418
|
+
"name": p.name,
|
|
419
|
+
"price": str(p.get_display_price()),
|
|
420
|
+
"stock": p.stock if not p.is_service else None,
|
|
421
|
+
"is_service": p.is_service,
|
|
422
|
+
}
|
|
423
|
+
for p in products
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def check_stock(product_id: int, quantity: int) -> bool:
|
|
428
|
+
try:
|
|
429
|
+
product = Product.objects.get(id=product_id)
|
|
430
|
+
except Product.DoesNotExist:
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
if product.is_service:
|
|
434
|
+
return True
|
|
435
|
+
return product.stock >= quantity
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### Value Objects
|
|
439
|
+
|
|
440
|
+
Immutable objects representing values:
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
from dataclasses import dataclass
|
|
444
|
+
from decimal import Decimal
|
|
445
|
+
|
|
446
|
+
@dataclass(frozen=True)
|
|
447
|
+
class Money:
|
|
448
|
+
amount: Decimal
|
|
449
|
+
currency: str = "ARS"
|
|
450
|
+
|
|
451
|
+
def __add__(self, other: "Money") -> "Money":
|
|
452
|
+
if self.currency != other.currency:
|
|
453
|
+
raise ValueError("Cannot add different currencies")
|
|
454
|
+
return Money(self.amount + other.amount, self.currency)
|
|
455
|
+
|
|
456
|
+
def __str__(self):
|
|
457
|
+
return f"{self.currency} {self.amount:.2f}"
|
|
458
|
+
|
|
459
|
+
# Usage
|
|
460
|
+
subtotal = Money(Decimal("1000.00"))
|
|
461
|
+
tax = Money(Decimal("210.00"))
|
|
462
|
+
total = subtotal + tax # Money(amount=Decimal('1210.00'), currency='ARS')
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## Utility Functions
|
|
466
|
+
|
|
467
|
+
### Common Helper Functions
|
|
468
|
+
|
|
469
|
+
```python
|
|
470
|
+
import os
|
|
471
|
+
import json
|
|
472
|
+
from datetime import datetime
|
|
473
|
+
from decimal import Decimal
|
|
474
|
+
from typing import Any, Dict
|
|
475
|
+
|
|
476
|
+
def safe_int(value: Any, default: int = 0) -> int:
|
|
477
|
+
"""Safely convert to int."""
|
|
478
|
+
try:
|
|
479
|
+
return int(value)
|
|
480
|
+
except (ValueError, TypeError):
|
|
481
|
+
return default
|
|
482
|
+
|
|
483
|
+
def safe_decimal(value: Any, default: Decimal = Decimal("0")) -> Decimal:
|
|
484
|
+
"""Safely convert to Decimal."""
|
|
485
|
+
try:
|
|
486
|
+
return Decimal(str(value))
|
|
487
|
+
except (ValueError, TypeError):
|
|
488
|
+
return default
|
|
489
|
+
|
|
490
|
+
def format_money(amount: Decimal, currency: str = "ARS") -> str:
|
|
491
|
+
"""Format money for display."""
|
|
492
|
+
return f"{currency} {amount:,.2f}".replace(",", ".")
|
|
493
|
+
|
|
494
|
+
def parse_json(body: bytes) -> Dict[str, Any]:
|
|
495
|
+
"""Parse JSON from request body."""
|
|
496
|
+
try:
|
|
497
|
+
return json.loads(body)
|
|
498
|
+
except json.JSONDecodeError:
|
|
499
|
+
raise ValueError("Invalid JSON")
|
|
500
|
+
|
|
501
|
+
def truncate(text: str, length: int = 100, suffix: str = "...") -> str:
|
|
502
|
+
"""Truncate text to length."""
|
|
503
|
+
if len(text) <= length:
|
|
504
|
+
return text
|
|
505
|
+
return text[:length - len(suffix)] + suffix
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Testing Utilities
|
|
509
|
+
|
|
510
|
+
```python
|
|
511
|
+
# tests/test_utils.py
|
|
512
|
+
import pytest
|
|
513
|
+
from decimal import Decimal
|
|
514
|
+
|
|
515
|
+
class TestSafeInt:
|
|
516
|
+
def test_valid_int(self):
|
|
517
|
+
assert safe_int("42") == 42
|
|
518
|
+
|
|
519
|
+
def test_invalid_returns_default(self):
|
|
520
|
+
assert safe_int("not a number") == 0
|
|
521
|
+
|
|
522
|
+
def test_custom_default(self):
|
|
523
|
+
assert safe_int("invalid", 100) == 100
|
|
524
|
+
|
|
525
|
+
class TestMoney:
|
|
526
|
+
def test_add_same_currency(self):
|
|
527
|
+
a = Money(Decimal("100"))
|
|
528
|
+
b = Money(Decimal("50"))
|
|
529
|
+
assert a + b == Money(Decimal("150"))
|
|
530
|
+
|
|
531
|
+
def test_different_currency_raises(self):
|
|
532
|
+
a = Money(Decimal("100"), "ARS")
|
|
533
|
+
b = Money(Decimal("50"), "USD")
|
|
534
|
+
with pytest.raises(ValueError):
|
|
535
|
+
_ = a + b
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Code to Avoid
|
|
539
|
+
|
|
540
|
+
### Mutable Default Arguments
|
|
541
|
+
|
|
542
|
+
```python
|
|
543
|
+
# WRONG — mutable default
|
|
544
|
+
def add_items(items=[]):
|
|
545
|
+
items.append(1)
|
|
546
|
+
return items
|
|
547
|
+
|
|
548
|
+
# CORRECT
|
|
549
|
+
def add_items(items=None):
|
|
550
|
+
if items is None:
|
|
551
|
+
items = []
|
|
552
|
+
items.append(1)
|
|
553
|
+
return items
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Shadowing Builtins
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
# WRONG
|
|
560
|
+
list = [1, 2, 3] # shadows built-in
|
|
561
|
+
id = "abc" # shadows built-in
|
|
562
|
+
|
|
563
|
+
# CORRECT
|
|
564
|
+
items = [1, 2, 3]
|
|
565
|
+
product_id = "abc"
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Magic Numbers
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
# WRONG
|
|
572
|
+
if price > 1000:
|
|
573
|
+
discount = price * 0.1
|
|
574
|
+
|
|
575
|
+
# CORRECT
|
|
576
|
+
DISCOUNT_THRESHOLD = Decimal("1000")
|
|
577
|
+
DISCOUNT_RATE = Decimal("0.1")
|
|
578
|
+
|
|
579
|
+
if price > DISCOUNT_THRESHOLD:
|
|
580
|
+
discount = price * DISCOUNT_RATE
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Modifying Collections in Loop
|
|
584
|
+
|
|
585
|
+
```python
|
|
586
|
+
# WRONG — modifies while iterating
|
|
587
|
+
for item in items:
|
|
588
|
+
if item.deleted:
|
|
589
|
+
items.remove(item)
|
|
590
|
+
|
|
591
|
+
# CORRECT
|
|
592
|
+
items = [item for item in items if not item.deleted]
|
|
593
|
+
# or
|
|
594
|
+
items_to_keep = []
|
|
595
|
+
for item in items:
|
|
596
|
+
if not item.deleted:
|
|
597
|
+
items_to_keep.append(item)
|
|
598
|
+
items = items_to_keep
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Resources
|
|
602
|
+
|
|
603
|
+
- **PEP 8** — Style Guide: https://peps.python.org/pep-0008/
|
|
604
|
+
- **PEP 484** — Type Hints: https://peps.python.org/pep-0484/
|
|
605
|
+
- **Real Python**: https://realpython.com/
|
|
606
|
+
- **Python Docs**: https://docs.python.org/3/
|