feat: add ponytail skill, examples, README
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
# API Endpoint
|
||||
|
||||
**Task:** "Add an endpoint that returns a user by id."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```
|
||||
app/
|
||||
├── controllers/user_controller.py
|
||||
├── services/user_service.py
|
||||
├── repositories/user_repository.py
|
||||
├── schemas/user_schemas.py
|
||||
└── exceptions/user_exceptions.py
|
||||
```
|
||||
|
||||
```python
|
||||
# schemas/user_schemas.py
|
||||
class UserResponseDTO(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
email: str
|
||||
|
||||
# repositories/user_repository.py
|
||||
class UserRepository:
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
def find_by_id(self, user_id: int) -> User | None:
|
||||
return self.db.get(User, user_id)
|
||||
|
||||
# services/user_service.py
|
||||
class UserService:
|
||||
def __init__(self, repo: UserRepository):
|
||||
self.repo = repo
|
||||
def get_user(self, user_id: int) -> User:
|
||||
user = self.repo.find_by_id(user_id)
|
||||
if user is None:
|
||||
raise UserNotFoundError(user_id)
|
||||
return user
|
||||
|
||||
# controllers/user_controller.py
|
||||
@router.get("/users/{user_id}", response_model=UserResponseDTO)
|
||||
def get_user(user_id: int, service: UserService = Depends(get_user_service)):
|
||||
try:
|
||||
return service.get_user(user_id)
|
||||
except UserNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
```
|
||||
|
||||
Five files, three classes, a custom exception, and a dependency-injection chain — wrapping one database call.
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```python
|
||||
# ponytail: it's one query
|
||||
@app.get("/users/{user_id}")
|
||||
def get_user(user_id: int, db: Session = Depends(get_db)):
|
||||
user = db.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(404)
|
||||
return user
|
||||
```
|
||||
|
||||
**5 files → 5 lines.** Layers earn their place when there are two implementations, not before. Add the service layer when a second caller shows up — if it ever does.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Caching System
|
||||
|
||||
**Task:** "We should cache these API responses."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```python
|
||||
import time
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheEntry:
|
||||
value: Any
|
||||
expires_at: float
|
||||
|
||||
|
||||
class TTLCache:
|
||||
"""Thread-safe LRU cache with per-entry TTL and max size."""
|
||||
|
||||
def __init__(self, max_size: int = 1000, default_ttl: float = 300.0):
|
||||
self._store: OrderedDict[str, CacheEntry] = OrderedDict()
|
||||
self._lock = threading.Lock()
|
||||
self.max_size = max_size
|
||||
self.default_ttl = default_ttl
|
||||
self.hits = 0
|
||||
self.misses = 0
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
with self._lock:
|
||||
entry = self._store.get(key)
|
||||
if entry is None or entry.expires_at < time.monotonic():
|
||||
self.misses += 1
|
||||
self._store.pop(key, None)
|
||||
return None
|
||||
self._store.move_to_end(key)
|
||||
self.hits += 1
|
||||
return entry.value
|
||||
|
||||
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
||||
with self._lock:
|
||||
if len(self._store) >= self.max_size:
|
||||
self._store.popitem(last=False)
|
||||
self._store[key] = CacheEntry(
|
||||
value, time.monotonic() + (ttl or self.default_ttl)
|
||||
)
|
||||
|
||||
# ... plus invalidation, stats endpoint, and unit tests for all of it
|
||||
```
|
||||
|
||||
120 lines of custom infrastructure for a problem nobody has measured yet.
|
||||
|
||||
## With Ponytail
|
||||
|
||||
First question: **do you actually need a cache?**
|
||||
|
||||
- **Unsure?** Ship without it. Add it when you measure the problem. (YAGNI)
|
||||
- **Pure function, hot path?** The standard library has it:
|
||||
|
||||
```python
|
||||
# ponytail: stdlib covers this
|
||||
from functools import lru_cache
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def fetch(key): ...
|
||||
```
|
||||
|
||||
- **Real distributed caching needs?** Use Redis / memcached / your platform's
|
||||
cache. Infrastructure problems get infrastructure, not a homemade class.
|
||||
|
||||
**120 lines → 0–3 lines.** The fastest cache is the one you didn't have to debug.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Date Picker
|
||||
|
||||
**Task:** "Add a date picker to this form."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```bash
|
||||
npm install flatpickr
|
||||
```
|
||||
|
||||
```jsx
|
||||
import flatpickr from "flatpickr";
|
||||
import "flatpickr/dist/flatpickr.min.css";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
export default function DatePicker({ value, onChange, minDate, maxDate }) {
|
||||
const inputRef = useRef(null);
|
||||
const instanceRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
instanceRef.current = flatpickr(inputRef.current, {
|
||||
defaultDate: value,
|
||||
minDate,
|
||||
maxDate,
|
||||
dateFormat: "Y-m-d",
|
||||
onChange: ([date]) => onChange(date),
|
||||
});
|
||||
return () => instanceRef.current?.destroy();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
instanceRef.current?.setDate(value, false);
|
||||
}, [value]);
|
||||
|
||||
return <input ref={inputRef} className="date-picker" />;
|
||||
}
|
||||
```
|
||||
|
||||
One dependency, one wrapper component, two `useEffect` hooks, a cleanup function, and a CSS import — to pick a date.
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```html
|
||||
<!-- ponytail: browser has one -->
|
||||
<input type="date">
|
||||
```
|
||||
|
||||
**1 dependency + 30 lines → 0 dependencies + 1 line.** Native, accessible, localized, keyboard-navigable, mobile-friendly. The browser team already did the work.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Email Validation
|
||||
|
||||
**Task:** "Validate an email address in Python."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```python
|
||||
import re
|
||||
|
||||
EMAIL_PATTERN = re.compile(
|
||||
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
)
|
||||
|
||||
class EmailValidator:
|
||||
"""Validates email addresses against RFC-like rules."""
|
||||
|
||||
def __init__(self, pattern: re.Pattern = EMAIL_PATTERN):
|
||||
self.pattern = pattern
|
||||
|
||||
def validate(self, email: str) -> bool:
|
||||
if not isinstance(email, str):
|
||||
raise TypeError("email must be a string")
|
||||
email = email.strip()
|
||||
if not email:
|
||||
return False
|
||||
return bool(self.pattern.match(email))
|
||||
|
||||
|
||||
def validate_email(email: str) -> bool:
|
||||
"""Convenience wrapper around EmailValidator."""
|
||||
return EmailValidator().validate(email)
|
||||
```
|
||||
|
||||
A class, a wrapper, a regex that still rejects valid addresses and accepts invalid ones. Regex cannot validate email. Only a delivery attempt can.
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```python
|
||||
# ponytail: good enough — real validation is sending the mail
|
||||
"@" in email and "." in email.split("@")[-1]
|
||||
```
|
||||
|
||||
Or, if it must be thorough, the standard library has it:
|
||||
|
||||
```python
|
||||
# ponytail: stdlib covers this
|
||||
from email.utils import parseaddr
|
||||
"@" in parseaddr(email)[1]
|
||||
```
|
||||
|
||||
**27 lines → 1 line.** And the honest answer: let the confirmation email reject it. That's what confirmation emails are for.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Sorting
|
||||
|
||||
**Task:** "Sort this array of numbers."
|
||||
|
||||
## Without Ponytail
|
||||
|
||||
```js
|
||||
function quickSort(arr, compareFn = (a, b) => a - b) {
|
||||
if (arr.length <= 1) return arr;
|
||||
|
||||
const pivot = arr[Math.floor(arr.length / 2)];
|
||||
const left = [];
|
||||
const right = [];
|
||||
const equal = [];
|
||||
|
||||
for (const item of arr) {
|
||||
const cmp = compareFn(item, pivot);
|
||||
if (cmp < 0) left.push(item);
|
||||
else if (cmp > 0) right.push(item);
|
||||
else equal.push(item);
|
||||
}
|
||||
|
||||
return [
|
||||
...quickSort(left, compareFn),
|
||||
...equal,
|
||||
...quickSort(right, compareFn),
|
||||
];
|
||||
}
|
||||
|
||||
const sorted = quickSort(numbers);
|
||||
```
|
||||
|
||||
A hand-rolled quicksort. It allocates three arrays per recursion level, blows the stack on large inputs, and replaces an engine-optimized native sort with homework.
|
||||
|
||||
## With Ponytail
|
||||
|
||||
```js
|
||||
// ponytail: this exists
|
||||
numbers.sort((a, b) => a - b)
|
||||
```
|
||||
|
||||
**24 lines → 1 line.** Every runtime ships a sort tuned by people whose whole job is sorting. Use it.
|
||||
Reference in New Issue
Block a user