mdlaw 1.0.0__tar.gz

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.
mdlaw-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pandamoon21
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mdlaw-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,610 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdlaw
3
+ Version: 1.0.0
4
+ Summary: Blazing-fast unofficial MyDramaList API wrapper (FastAPI + httpx, official MDL Android app API)
5
+ Author: pandamoon21
6
+ License: MIT
7
+ Keywords: mydramalist,mdl,drama,k-drama,api,wrapper,fastapi
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: fastapi>=0.110
21
+ Requires-Dist: uvicorn[standard]>=0.29
22
+ Requires-Dist: httpx>=0.27
23
+ Provides-Extra: mysql
24
+ Requires-Dist: pymysql>=1.1; extra == "mysql"
25
+ Provides-Extra: postgres
26
+ Requires-Dist: psycopg[binary]>=3.1; extra == "postgres"
27
+ Dynamic: license-file
28
+
29
+ # mdlaw
30
+
31
+ **Blazing-fast unofficial API for [MyDramaList](https://mydramalist.com)**, powered by the **official MDL Android app API** (`app-api.mydramalist.com/v1`).
32
+
33
+ Built from reverse-engineering `com.mydramalist.app` v2.3.18 (Flutter/Dio): the app's request header scheme, auth flow, and endpoint inventory were recovered and live-validated before this wrapper was written.
34
+
35
+ > ⚠️ **Unofficial.** Not affiliated with MyDramaList. For research & personal use. Stop using if an official API ships.
36
+
37
+ ---
38
+
39
+ ## ✨ Why mdlaw?
40
+
41
+ | | Web scrapers | **mdlaw** |
42
+ |---|---|---|
43
+ | Data source | HTML scraping (`mydramalist.com`) | **Official app JSON API** |
44
+ | Speed | Slow — parses HTML per request | **~1 ms** after cache warm-up |
45
+ | Fragility | Breaks when site markup changes | Stable — same API the app uses |
46
+ | WAF risk | High | Throttled + cached outbound |
47
+
48
+ The Android app talks to a JSON API behind Cloudflare. `mdlaw` replicates the exact request headers the app sends (validated: without them the API returns **403**), so it gets **real structured data** — not scraped HTML.
49
+
50
+ ## 📊 Real-world benchmark (measured, not estimated)
51
+
52
+ Same machine, same network, live hits (median of 3 runs):
53
+
54
+ | Test | Latency | Payload |
55
+ |---|---|---|
56
+ | **mdlaw** `GET /api/v1/genres` (cached) | **2.9 ms** | 35 genres · 1.7 KB JSON |
57
+ | **mdlaw** `GET /api/v1/titles/1/reviews` (cold — hits upstream) | **298 ms** | 5 reviews JSON |
58
+ | **Web scraper** fetch `mydramalist.com/` + parse HTML | **395 ms** | 112 KB HTML |
59
+
60
+ **Result: mdlaw (cached) is ~136× faster** than a typical web scraper, and even a *cold* mdlaw hit (real upstream call) beats HTML scraping. On top of that, scrapers return HTML that you still have to parse, while mdlaw hands you clean JSON.
61
+
62
+ > Benchmark: 2026-09-01, `python3` + `urllib` on localhost → mydramalist.com. Your numbers will vary with network, but the order of magnitude is the point.
63
+
64
+ ---
65
+
66
+ ## 🤖 AI-agent instructions
67
+
68
+ `AGENTS.md` is a purpose-built instruction file for AI coding agents (and new
69
+ developers): how to install, verify, extend, and deploy `mdlaw` without
70
+ tripping on the WAF/throttle/cache rules.
71
+
72
+ - **File**: [`AGENTS.md`](./AGENTS.md)
73
+ - It covers: quick start, verification commands, env var reference, how the
74
+ throttling/cache/auth works, conventions & pitfalls (never remove the
75
+ throttle, keep tests offline, don't leak credentials), and deploy steps.
76
+
77
+ Point any agent (or your future self) at `AGENTS.md` first — it's the fastest
78
+ path to a working setup.
79
+
80
+ ---
81
+
82
+ ## 🚀 Quickstart
83
+
84
+ **Option 1 — pip (PyPI):**
85
+
86
+ ```bash
87
+ pip install mdlaw
88
+ export MDL_API_KEY=<your-key> # REQUIRED — no key embedded in the public build
89
+ mdlaw # serves http://0.0.0.0:8000 (Swagger at /docs)
90
+ mdlaw self # offline self-check (needs MDL_API_KEY)
91
+ ```
92
+
93
+ **Option 2 — from source:**
94
+
95
+ ```bash
96
+ # 1. Clone & enter
97
+ git clone <your-repo-url> mdlaw && cd mdlaw
98
+
99
+ # 2. Create venv & install
100
+ python3 -m venv .venv && . .venv/bin/activate
101
+ pip install -r requirements.txt
102
+
103
+ # 3. Run
104
+ uvicorn mdlaw:app --port 8000
105
+ ```
106
+
107
+ Verify:
108
+
109
+ ```bash
110
+ curl http://localhost:8000/api/v1/health
111
+ # {"status":"ok","service":"mdlaw"}
112
+
113
+ curl http://localhost:8000/api/v1/genres | python3 -m json.tool
114
+ ```
115
+
116
+ Interactive docs (Swagger UI): **http://localhost:8000/docs**
117
+
118
+ ## 📚 API docs & playground
119
+
120
+ `mdlaw` ships with the industry-standard docs out of the box (FastAPI built-ins — no extra setup):
121
+
122
+ | URL | What it is |
123
+ |---|---|
124
+ | **`/`** | Redirects to Swagger UI |
125
+ | **`/docs`** | **Swagger UI — interactive playground.** "Try it out" on any endpoint, fill params, execute live, see the JSON response + latency. |
126
+ | **`/redoc`** | ReDoc — clean, readable reference docs |
127
+ | **`/openapi.json`** | Machine-readable OpenAPI 3.1 spec (for codegen / clients) |
128
+
129
+ The Swagger playground is pre-configured for quick use: *Try it out* is open by default, request duration is displayed, and the schemas section is collapsed so endpoints are front and center.
130
+
131
+ ## 🖥 Deploy locally
132
+
133
+ Three options, same code.
134
+
135
+ ### Option 1 — Run directly (dev)
136
+
137
+ ```bash
138
+ python3 -m venv .venv && . .venv/bin/activate
139
+ pip install -r requirements.txt
140
+ uvicorn mdlaw:app --host 0.0.0.0 --port 8000
141
+ # → http://localhost:8000/docs
142
+ ```
143
+
144
+ ### Option 2 — Docker
145
+
146
+ ```bash
147
+ docker build -t mdlaw .
148
+ docker run -p 8000:8000 mdlaw
149
+ ```
150
+
151
+ ### Option 3 — Docker Compose (production-ish)
152
+
153
+ ```yaml
154
+ # docker-compose.yml
155
+ services:
156
+ mdlaw:
157
+ build: .
158
+ ports:
159
+ - "8000:8000"
160
+ environment:
161
+ - MDL_API_KEY=${MDL_API_KEY} # optional
162
+ restart: unless-stopped
163
+ ```
164
+
165
+ ```bash
166
+ docker compose up -d
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 📡 Endpoints
172
+
173
+ All responses are the **raw upstream JSON** (fastest path, zero transformation).
174
+
175
+ | Method | Route | Upstream | Cache TTL | Auth |
176
+ |---|---|---|---|---|
177
+ | GET | `/api/v1/health` | — | — | — |
178
+ | GET | `/api/v1/dashboard` | — *(local)* | — | — |
179
+ | GET | `/api/v1/status` | — *(alias dashboard)* | — | — |
180
+ | GET | `/api/v1/auth/status` | — *(local)* | — | — |
181
+ | GET | `/api/v1/cache/stats` | — *(local)* | — | — |
182
+ | POST | `/api/v1/auth/login` | `POST /auth/login` *(force re-login)* | — | env |
183
+ | POST | `/api/v1/auth/refresh` | `POST /auth/refresh` *(force refresh)* | — | env |
184
+ | GET | `/api/v1/genres` | `GET /genres` | 1h | — |
185
+ | GET | `/api/v1/languages` | `GET /languages/supported?v=2` | 1h | — |
186
+ | GET | `/api/v1/titles/{id}` | `GET /titles/{id}?expand=1` | 5m | ✅ |
187
+ | GET | `/api/v1/titles/{id}/reviews` | `GET /titles/{id}/reviews` | 5m | — |
188
+ | GET | `/api/v1/titles/{id}/comments` | `GET /titles/{id}/comments` | 5m | — |
189
+ | GET | `/api/v1/titles/{id}/credits` | `GET /titles/{id}/credits` | 5m | — |
190
+ | GET | `/api/v1/titles/{id}/recommendations` | `GET /titles/{id}/recommendations` | 10m | ✅ |
191
+ | POST | `/api/v1/search?q={q}` | `POST /search` (body `{"q", "synopsis"}`) | 5m | ✅ |
192
+ | POST | `/api/v1/search/people?q={q}` | `POST /search/people` (body `{"q"}`) | 5m | ✅ |
193
+ | GET | `/api/v1/watchlist` | `GET /sync/mylist/watchlist` | 1m | ✅ |
194
+ | GET | `/api/v1/watchlist/{status}` | `GET /sync/mylist/{status}` | 1m | ✅ |
195
+ | GET | `/api/v1/me` | `GET /users/me` | 1m | ✅ |
196
+ | GET | `/api/v1/calendar` | `POST /calendar/episodes` | 1h | — |
197
+ | GET | `/api/v1/articles/featured?page={page}` | `GET /articles/featured` | 10m | — |
198
+ | GET | `/api/v1/lists/featured?limit={limit}` | `GET /lists/featured` | 10m | — |
199
+ | GET | `/api/v1/lists/popular?limit={limit}` | `GET /lists/popular_voting_lists` | 10m | — |
200
+ | GET | `/api/v1/people/leaderboard?period={alltime\|weekly\|monthly}` | `GET /people/leaderboard` | 10m | — |
201
+ | GET | `/api/v1/people/{id}` | `GET /people/{id}` | 24h | — |
202
+ | GET | `/api/v1/payment/plans` | `GET /payment/plans` | 1h | — |
203
+ | GET | `/api/v1/payment/coins` | `GET /payment/coins` | 1h | — |
204
+
205
+ > **Auth ✅** = needs `MDL_USERNAME` + `MDL_PASSWORD` (see [🔐 Account](#-account)). Without them these return `400` with a clear message.
206
+
207
+ ### Example responses (captured live)
208
+
209
+ **`GET /api/v1/genres`** → `200` · `Cache-Control: public, max-age=3600`
210
+
211
+ ```json
212
+ [
213
+ { "id": 1, "name": "Action", "slug": "action" },
214
+ { "id": 5, "name": "Adventure", "slug": "adventure" },
215
+ { "id": 13, "name": "Business", "slug": "business" }
216
+ ]
217
+ ```
218
+
219
+ **`GET /api/v1/people/6997`** → `200`
220
+
221
+ ```json
222
+ {
223
+ "id": 6997,
224
+ "name": "Shin Myung Jin",
225
+ "first_name": "Myung Jin",
226
+ "family_name": "Shin",
227
+ "biography": "A South Korean actor. Starred in the movie December (2014).",
228
+ "permalink": "https://mydramalist.com/people/6997-shin-myung-jin",
229
+ "nationality": "South Korean",
230
+ "ranked": 99999,
231
+ "images": {
232
+ "thumb": "https://i.mydramalist.com/QDldg_5t.jpg",
233
+ "medium": "https://i.mydramalist.com/QDldg_5m.jpg",
234
+ "poster": "https://i.mydramalist.com/QDldg_5c.jpg"
235
+ }
236
+ }
237
+ ```
238
+
239
+ **`GET /api/v1/titles/1/reviews`** → `200`
240
+
241
+ ```json
242
+ [
243
+ {
244
+ "id": 3561,
245
+ "ratings": { "story": 10, "acting": 10, "music": 10, "rewatch": 10, "overall": 10 },
246
+ "headline": "As I watched through the whole...",
247
+ "upvotes": 0,
248
+ "total_votes": 0,
249
+ "spoiler": false,
250
+ "lang_iso": "en"
251
+ }
252
+ ]
253
+ ```
254
+
255
+ **`GET /api/v1/calendar`** → `200` — airing calendar
256
+
257
+ ```json
258
+ {
259
+ "items": [
260
+ {
261
+ "id": 4349807, "rid": 695673, "episode_number": 19,
262
+ "released_at": 1788141600, "duration": 19,
263
+ "permalink": "/695673-wu-zuo-nu-fu-ma/episode/19"
264
+ }
265
+ ],
266
+ "relationships": []
267
+ }
268
+ ```
269
+
270
+ ### Errors
271
+
272
+ | Case | Status | Body |
273
+ |---|---|---|
274
+ | Upstream 4xx (e.g. auth-gated) | `400`/`401`/`403` | `{"error": true, "code": <status>, "detail": <upstream body>}` |
275
+ | Upstream timeout | `504` | `{"error": true, "code": 504, "detail": "upstream timeout"}` |
276
+ | Upstream unreachable | `502` | `{"error": true, "code": 502, "detail": "upstream error: ..."}` |
277
+ | Invalid query param | `400` | `{"error": true, "code": 400, "detail": "period must be alltime\|weekly\|monthly"}` |
278
+ | Bad path / type | `404` / `422` | FastAPI default |
279
+
280
+ ---
281
+
282
+ ## ⚡ Performance
283
+
284
+ - **Async + connection pooling** — one `httpx.AsyncClient` reuses TCP/TLS connections.
285
+ - **TTL cache in-memory** — repeat requests never hit upstream. Measured:
286
+ - Cold (first hit): **~0.5 s**
287
+ - Warm (cached): **~1 ms**
288
+ - **`Cache-Control` header** on every response → CDN/proxy caching works too.
289
+ - **Outbound throttle** (2 concurrent, 0.5 s min interval) — keeps us under MDL's WAF rate limit.
290
+
291
+ > **Note:** the in-memory cache is single-process. Swap to a SQL backend or Redis if you run more than one instance.
292
+
293
+ ---
294
+
295
+ ## 💾 Response cache (DB)
296
+
297
+ By default `mdlaw` caches responses **in memory** (TTL). For persistence across restarts / multiple instances, switch to a **SQL database**:
298
+
299
+ | Env | Value | Notes |
300
+ |---|---|---|
301
+ | `MDL_CACHE_BACKEND` | `memory` (default) · `sqlite` · `mysql` · `postgres` | `memory` needs no setup |
302
+ | `MDL_CACHE_DB_URL` | e.g. `sqlite:///mdlaw_cache.db` · `mysql://user:pass@host/db` · `postgresql://user:pass@host/db` | required when backend ≠ memory |
303
+
304
+ ```bash
305
+ # SQLite (zero setup — stdlib driver)
306
+ export MDL_CACHE_BACKEND=sqlite
307
+ export MDL_CACHE_DB_URL=sqlite:///mdlaw_cache.db
308
+ uvicorn mdlaw:app --port 8000
309
+
310
+ # MySQL
311
+ pip install pymysql
312
+ export MDL_CACHE_BACKEND=mysql MDL_CACHE_DB_URL=mysql://user:pass@localhost:3306/mdlaw
313
+ uvicorn mdlaw:app --port 8000
314
+
315
+ # PostgreSQL
316
+ pip install "psycopg[binary]"
317
+ export MDL_CACHE_BACKEND=postgres MDL_CACHE_DB_URL=postgresql://user:pass@localhost:5432/mdlaw
318
+ uvicorn mdlaw:app --port 8000
319
+ ```
320
+
321
+ The cache table (`mdlaw_cache`) stores each response as JSON + a **sha256 hash**.
322
+
323
+ ### 🔄 How response changes are detected
324
+
325
+ MDL doesn't push changes — so `mdlaw` uses the standard **TTL + hash comparison** pattern:
326
+
327
+ 1. Response is stored with its sha256 hash.
328
+ 2. When the TTL expires, the upstream is re-fetched.
329
+ 3. The new hash is compared to the stored one:
330
+ - **Different** → the row is updated and flagged `changed=1` (with `updated_at`).
331
+ - **Same** → the row is refreshed (TTL extended), `changed=0`.
332
+ 4. So you always know *when a response actually changed* — not just when it was re-fetched.
333
+
334
+ Inspect live:
335
+
336
+ ```bash
337
+ curl http://localhost:8000/api/v1/cache/stats
338
+ # {"backend": "sqlite", "hits": 1, "misses": 1, "hit_rate": 0.5, "entries": 1}
339
+ ```
340
+
341
+ > **Note:** change detection is only meaningful for a persistent backend (sqlite/mysql/postgres). In-memory cache resets on restart, so nothing to compare against.
342
+
343
+ ---
344
+
345
+ ## ⚙️ Configuration requirements
346
+
347
+ Everything `mdlaw` needs is set via environment variables. Here's the full picture at a glance:
348
+
349
+ | Variable | Required? | What it does | Default |
350
+ |---|---|---|---|
351
+ | `MDL_API_KEY` | **Yes** | The MDL app API key (not in source — you provide it) | — |
352
+ | `MDL_USERNAME` | No | Enables auth-gated endpoints (title detail, search, watchlist, `/me`) | disabled |
353
+ | `MDL_PASSWORD` | No | Password for the account above (paired with `MDL_USERNAME`) | disabled |
354
+ | `MDL_CACHE_BACKEND` | No | `memory` · `sqlite` · `mysql` · `postgres` | `memory` |
355
+ | `MDL_CACHE_DB_URL` | Only if backend ≠ memory | DSN, e.g. `sqlite:///mdlaw_cache.db` | — |
356
+
357
+ ### 🔐 API key (required)
358
+
359
+ The MDL Android app talks to `app-api.mydramalist.com` with a hardcoded
360
+ `mdl-api-key` header. This public build does **not** embed that key — you must
361
+ provide it yourself (extract from the APK, or get it from a collaborator
362
+ who has access to the app key). The server **refuses to start** without `MDL_API_KEY`:
363
+
364
+ ```bash
365
+ export MDL_API_KEY=<your-key>
366
+ uvicorn mdlaw:app --port 8000
367
+ ```
368
+
369
+ ### 👤 Account (optional)
370
+
371
+ Set both to unlock auth-gated endpoints:
372
+
373
+ ```bash
374
+ export MDL_USERNAME=your_username
375
+ export MDL_PASSWORD=your_password
376
+ uvicorn mdlaw:app --port 8000
377
+ ```
378
+
379
+ Without them, auth-gated endpoints return a clear `400` ("requires MDL_USERNAME and MDL_PASSWORD env vars").
380
+
381
+ ### 💾 Cache (optional)
382
+
383
+ ```bash
384
+ # default (in-memory) — no setup
385
+ uvicorn mdlaw:app --port 8000
386
+
387
+ # persistent (SQLite, zero dependencies)
388
+ export MDL_CACHE_BACKEND=sqlite MDL_CACHE_DB_URL=sqlite:///mdlaw_cache.db
389
+ uvicorn mdlaw:app --port 8000
390
+ ```
391
+
392
+ ---
393
+
394
+ ## 🔐 Account (auth) — how it works
395
+
396
+ 1. `POST /auth/login?device_id=<uuid>` with `{username, password: md5(password)}` → `{token, refresh_token, user}`.
397
+ 2. Token is a JWT; `exp` is decoded to know when to refresh.
398
+ 3. On `401` (or `invalid_grant`), `POST /auth/refresh` is called; if that fails, it re-logs in.
399
+
400
+ Check status:
401
+
402
+ ```bash
403
+ curl http://localhost:8000/api/v1/auth/status
404
+ # {"configured": true, "logged_in": true, "user": {...}, "expires_at": ..., "refreshes": 0, ...}
405
+ ```
406
+
407
+ > ⚠️ **2FA is not supported.** If your account has 2FA enabled, either disable it or use an app password. The wrapper will return `428` ("2FA required") instead of hanging.
408
+
409
+ ---
410
+
411
+ ## 🚢 Deploy
412
+
413
+ All options below. This is the **public** build: `MDL_API_KEY` is **required**
414
+ everywhere — the app refuses to start without it.
415
+
416
+ | Env | Public build |
417
+ |---|---|
418
+ | `MDL_API_KEY` | **required** |
419
+ | `MDL_USERNAME` + `MDL_PASSWORD` | optional (auth-gated endpoints) |
420
+ | `MDL_CACHE_BACKEND` + `MDL_CACHE_DB_URL` | optional (persistent cache) |
421
+
422
+ ### Option A — Fly.io (recommended)
423
+
424
+ ```bash
425
+ # 1. One-time: create the app (reads fly.toml)
426
+ fly launch --no-deploy
427
+
428
+ # 2. Set secrets (MDL_API_KEY REQUIRED)
429
+ fly secrets set MDL_API_KEY=<your-key>
430
+ fly secrets set MDL_USERNAME=<your-user> MDL_PASSWORD=<your-pass> # optional
431
+ fly secrets set MDL_CACHE_BACKEND=sqlite MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db # optional
432
+
433
+ # 3. Deploy
434
+ fly deploy
435
+ ```
436
+
437
+ `fly.toml` is preconfigured: port 8000, region `sin`, **1 machine always
438
+ running** (no cold starts), HTTPS forced.
439
+
440
+ > **Persistent cache on Fly.io**: the default in-memory cache resets on every
441
+ > deploy/restart. Use the SQLite backend with a volume if you want it to
442
+ > survive restarts:
443
+ >
444
+ > ```bash
445
+ > fly volumes create mdlaw_data --size 1
446
+ > fly secrets set MDL_CACHE_BACKEND=sqlite MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db
447
+ > # then add to fly.toml:
448
+ > # [mounts]
449
+ > # source = "mdlaw_data"
450
+ > # destination = "/data"
451
+ > fly deploy
452
+ > ```
453
+
454
+ ### Option B — Vercel (serverless)
455
+
456
+ Works, **but know the trade-offs**:
457
+
458
+ ```bash
459
+ npx vercel
460
+ # when prompted, set env vars in the Vercel dashboard:
461
+ # Settings → Environment Variables → add MDL_API_KEY (REQUIRED),
462
+ # MDL_USERNAME, MDL_PASSWORD, MDL_CACHE_BACKEND, MDL_CACHE_DB_URL
463
+ ```
464
+
465
+ Two files are already included (`api/index.py` + `vercel.json`). Caveats:
466
+
467
+ - **Cold starts**: Python serverless functions boot per request — first hit after idle is slow (~2–5 s).
468
+ - **Cache is per-instance & ephemeral**: the TTL cache resets between cold starts, so MDL's WAF sees more upstream traffic than on a persistent host. Still throttled (2 concurrent, 0.5 s interval), so it holds up, but it's not "blazing" on first hits.
469
+ - **SQL cache not recommended on Vercel**: serverless has no persistent filesystem — use an external MySQL/Postgres if you want persistence.
470
+ - Fine for a demo / low-traffic personal API. For consistent speed, use **Fly.io** (persistent instance, warm cache).
471
+
472
+ ### Option C — Docker
473
+
474
+ ```bash
475
+ docker build -t mdlaw .
476
+
477
+ # MDL_API_KEY REQUIRED
478
+ docker run -p 8000:8000 \
479
+ -e MDL_API_KEY=<your-key> \
480
+ -e MDL_USERNAME=<your-user> -e MDL_PASSWORD=<your-pass> \
481
+ -e MDL_CACHE_BACKEND=sqlite -e MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db \
482
+ -v mdlaw_data:/data \
483
+ mdlaw
484
+ ```
485
+
486
+ ### Option D — Docker Compose (production)
487
+
488
+ ```yaml
489
+ # docker-compose.yml
490
+ services:
491
+ mdlaw:
492
+ build: .
493
+ ports:
494
+ - "8000:8000"
495
+ environment:
496
+ MDL_API_KEY: ${MDL_API_KEY} # required
497
+ MDL_USERNAME: ${MDL_USERNAME} # optional
498
+ MDL_PASSWORD: ${MDL_PASSWORD} # optional
499
+ MDL_CACHE_BACKEND: ${MDL_CACHE_BACKEND:-memory}
500
+ MDL_CACHE_DB_URL: ${MDL_CACHE_DB_URL:-}
501
+ volumes:
502
+ - mdlaw_data:/data
503
+ restart: unless-stopped
504
+
505
+ volumes:
506
+ mdlaw_data:
507
+ ```
508
+
509
+ ```bash
510
+ # .env (same folder, gitignored)
511
+ MDL_API_KEY=your-key
512
+ MDL_USERNAME=your-user
513
+ MDL_PASSWORD=your-pass
514
+ MDL_CACHE_BACKEND=sqlite
515
+ MDL_CACHE_DB_URL=sqlite:////data/mdlaw_cache.db
516
+
517
+ docker compose up -d
518
+ ```
519
+
520
+ ### Option E — VPS / systemd
521
+
522
+ Any host that can run Python 3.12.
523
+
524
+ ```bash
525
+ # /etc/systemd/system/mdlaw.service
526
+ [Unit]
527
+ Description=mdlaw API
528
+ After=network.target
529
+
530
+ [Service]
531
+ WorkingDirectory=/opt/mdlaw
532
+ ExecStart=/opt/mdlaw/.venv/bin/uvicorn mdlaw:app --host 0.0.0.0 --port 8000
533
+ EnvironmentFile=/opt/mdlaw/.env
534
+ Restart=always
535
+ User=mdlaw
536
+
537
+ [Install]
538
+ WantedBy=multi-user.target
539
+ ```
540
+
541
+ ```bash
542
+ sudo systemctl daemon-reload
543
+ sudo systemctl enable --now mdlaw
544
+ # logs: journalctl -u mdlaw -f
545
+ ```
546
+
547
+ ---
548
+
549
+ ## 🧪 Tests
550
+
551
+ Offline — no network needed:
552
+
553
+ ```bash
554
+ python -m pytest tests/ -q
555
+ # 8 passed
556
+ ```
557
+
558
+ Covers: key decode, md5 formula, header scheme, TTL cache expiry, SQL cache change detection, route count, auth-offline, and leaderboard validation.
559
+
560
+ ---
561
+
562
+ ## 📦 Project layout
563
+
564
+ ```
565
+ mdlaw/
566
+ ├── mdlaw.py # the whole API: config, client, cache (memory/SQL), auth, 27 routes
567
+ ├── pyproject.toml # PyPI packaging (pip install mdlaw → console script)
568
+ ├── LICENSE # MIT
569
+ ├── AGENTS.md # AI-agent / dev instructions (install, verify, extend, pitfalls)
570
+ ├── CHANGELOG.md # version history (Keep a Changelog convention)
571
+ ├── api/index.py # Vercel serverless entrypoint
572
+ ├── vercel.json # Vercel config (rewrites, function limits)
573
+ ├── requirements.txt # fastapi, uvicorn[standard], httpx
574
+ ├── Dockerfile # python:3.12-slim, non-root
575
+ ├── docker-compose.yml # one-command production deploy (with healthcheck)
576
+ ├── fly.toml # Fly.io config (port 8000, region sin, always-on)
577
+ ├── pytest.ini # pythonpath for tests
578
+ ├── .env.example # API key + account + cache backend placeholders
579
+ ├── tests/
580
+ │ └── test_mdlaw.py # 8 offline checks
581
+ └── README.md
582
+ ```
583
+
584
+ ---
585
+
586
+ ## ⚠️ Known limits
587
+
588
+ - **2FA accounts** are not supported by the auto-login (returns `428`). Disable 2FA or use an app password.
589
+ - **Anonymous guest mode does not exist** — MDL's Firebase project has anonymous auth disabled. Account features need `MDL_USERNAME`/`MDL_PASSWORD`.
590
+ - **One account per instance** — the wrapper holds a single token for all requests.
591
+ - **Rate limits**: the MDL WAF soft-blocks bursts. `mdlaw` throttles outbound, but don't point heavy crawlers at it.
592
+ - **Version drift**: if MDL updates the app's header scheme / key, endpoints may start returning 403. Update `mdlaw.py` constants (single place).
593
+
594
+ ---
595
+
596
+ ## 🛠 Development
597
+
598
+ ```bash
599
+ # self-check (offline)
600
+ python mdlaw.py self
601
+
602
+ # run dev server with auto-reload
603
+ uvicorn mdlaw:app --reload --port 8000
604
+ ```
605
+
606
+ ---
607
+
608
+ ## 📜 License & disclaimer
609
+
610
+ Unofficial, for research/education. Not affiliated with MyDramaList. All data © MyDramaList. Use at your own risk; stop if an official API ships.