feat: add ponytail skill, examples, README

This commit is contained in:
Emeriko
2026-06-12 03:05:59 +02:00
parent 3a3d78d1b9
commit ef604945d8
7 changed files with 455 additions and 1 deletions
+63
View File
@@ -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.
+74
View File
@@ -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 → 03 lines.** The fastest cache is the one you didn't have to debug.
+48
View File
@@ -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.
+51
View File
@@ -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.
+42
View File
@@ -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.