91 lines
2.9 KiB
Markdown
91 lines
2.9 KiB
Markdown
|
A
|
# Unit 01 — Database Layer
|
||
|
|
|
||
|
|
## Goal
|
||
|
|
SQLite persistence layer with CRUD operations and MAC-address-based rate limiting.
|
||
|
|
|
||
|
|
## Files
|
||
|
|
- `server/database.py`
|
||
|
|
- `server/models.py`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `server/models.py`
|
||
|
|
|
||
|
|
```python
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from datetime import datetime
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Subscriber:
|
||
|
|
id: Optional[int]
|
||
|
|
imsi: str
|
||
|
|
iccid: str
|
||
|
|
ki: str # hex string
|
||
|
|
opc: str # hex string
|
||
|
|
mac_address: Optional[str]
|
||
|
|
created_at: datetime
|
||
|
|
synced_to_core: bool
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## `server/database.py`
|
||
|
|
|
||
|
|
### Schema
|
||
|
|
```sql
|
||
|
|
CREATE TABLE IF NOT EXISTS subscribers (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
imsi TEXT UNIQUE NOT NULL,
|
||
|
|
iccid TEXT UNIQUE NOT NULL,
|
||
|
|
ki TEXT NOT NULL,
|
||
|
|
opc TEXT NOT NULL,
|
||
|
|
mac_address TEXT,
|
||
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
|
|
synced_to_core BOOLEAN DEFAULT 0
|
||
|
|
);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_imsi ON subscribers(imsi);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_mac_address ON subscribers(mac_address);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Functions
|
||
|
|
|
||
|
|
| Function | Signature | Returns | Notes |
|
||
|
|
|----------|-----------|---------|-------|
|
||
|
|
| `init_db` | `(db_path: str) -> None` | — | Creates schema idempotently |
|
||
|
|
| `get_connection` | `(db_path: str) -> sqlite3.Connection` | connection | `row_factory = sqlite3.Row` |
|
||
|
|
| `add_subscriber` | `(conn, imsi, iccid, ki, opc, mac_address) -> int` | row `id` | Raises `ValueError` on duplicate IMSI/ICCID |
|
||
|
|
| `get_subscriber_by_imsi` | `(conn, imsi: str) -> Optional[Subscriber]` | Subscriber or None | |
|
||
|
|
| `get_subscriber_by_id` | `(conn, sub_id: int) -> Optional[Subscriber]` | Subscriber or None | |
|
||
|
|
| `list_unsynced` | `(conn) -> list[Subscriber]` | list | WHERE synced_to_core = 0 |
|
||
|
|
| `mark_synced` | `(conn, imsi: str) -> None` | — | UPDATE synced_to_core = 1 |
|
||
|
|
| `check_rate_limit` | `(conn, mac_address: str, window_hours: int) -> bool` | True = allowed | Counts rows for mac in last N hours |
|
||
|
|
| `export_all` | `(conn) -> list[dict]` | list of dicts | All rows as serializable dicts |
|
||
|
|
|
||
|
|
### Rate Limiting Logic
|
||
|
|
```python
|
||
|
|
def check_rate_limit(conn, mac_address, window_hours):
|
||
|
|
cutoff = datetime.utcnow() - timedelta(hours=window_hours)
|
||
|
|
cur = conn.execute(
|
||
|
|
"SELECT COUNT(*) FROM subscribers WHERE mac_address = ? AND created_at > ?",
|
||
|
|
(mac_address, cutoff.isoformat())
|
||
|
|
)
|
||
|
|
count = cur.fetchone()[0]
|
||
|
|
return count == 0 # True = allowed (no prior provisioning in window)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Verification
|
||
|
|
```bash
|
||
|
|
python -c "
|
||
|
|
from server.database import init_db, get_connection, add_subscriber, check_rate_limit
|
||
|
|
import tempfile, os
|
||
|
|
db = tempfile.mktemp(suffix='.db')
|
||
|
|
init_db(db)
|
||
|
|
conn = get_connection(db)
|
||
|
|
sid = add_subscriber(conn, '302720000000001', '8910302720000000001', 'aa'*16, 'bb'*16, 'aa:bb:cc:dd:ee:ff')
|
||
|
|
print('inserted id:', sid)
|
||
|
|
print('rate limit (should be False now):', check_rate_limit(conn, 'aa:bb:cc:dd:ee:ff', 1))
|
||
|
|
conn.close(); os.unlink(db)
|
||
|
|
"
|
||
|
|
```
|