-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Three layers that never reach across the wrong way:
-
poketrack.core— data layer: models, database, parser, scheduler, service. Knows nothing about any UI (no Tk, no Flask imports). -
poketrack.gui— legacy desktop presentation (CustomTkinter). -
poketrack.web— web presentation (Flask). -
desktop/— the native Tauri desktop shell (Rust). Not a fourth UI layer in the Python sense — it spawnspoketrack.web's Flask server as a sidecar process and opens a native window pointed at it, so it reusespoketrack.webentirely rather than reimplementing presentation. See Desktop Shell.
Both Python front-ends talk only to PokeTrackService (core/service.py)
— never to the parser or database directly. That's the single seam that keeps
fetching/parsing fully separated from presentation.
┌──────────────────────────────┐
│ PokeTrackService │ ← the only thing both UIs touch
│ (core/service.py) │
└───┬───────────┬───────────┬───┘
parser.py │ database │ scheduler│
(LeekDuck/Blog)│ (SQLite) │ (APSched) │
▼ ▼ ▼
┌───────────────┐ ┌──────────────────┐
│ gui/app.py │ │ web/server.py │
│ CustomTkinter │ │ Flask │
└───────────────┘ └──────────────────┘
▲さんかく ▲さんかく
└──── theme.py ─────┘ (one Midnight Blue palette)
poketrack/
├── app_context.py # builds the shared service; RESOURCE_ROOT vs ROOT (frozen-aware)
├── config.py # config.json manager (deep-merge over defaults, dotted-path get/set)
├── i18n.py # Translator — dotted-key lookup, current→English→key fallback
├── core/
│ ├── models.py # Event dataclass — parsing, status/countdown logic, (de)serialisation
│ ├── regions.py # region constants + keyword classifier (data/regions_map.json)
│ ├── database.py # SQLite persistence — thread-safe (connection-per-op), WAL, migrations
│ ├── http.py # shared requests.Session with retry/backoff + User-Agent
│ ├── parser.py # LeekDuckSource (JSON) + PokemonGoBlogSource (HTML), sync + async fetch
│ ├── native.py # loader for the optional Rust fast path (guarded import)
│ ├── notify.py # optional desktop notifications (plyer, guarded)
│ ├── webhook.py # outgoing webhooks — payload shaping + HMAC signing
│ ├── telegram.py # Telegram Bot API sendMessage wrapper
│ ├── calendar.py # hand-rolled iCalendar (.ics) builder
│ ├── updates.py # best-effort GitHub-releases update check (fail-silent)
│ ├── asyncrunner.py # background asyncio event loop for the desktop GUI
│ ├── scheduler.py # APScheduler wrapper (interval trigger, safe job wrapper)
│ └── service.py # PokeTrackService — the shared controller (see below)
├── gui/
│ ├── theme.py # MIDNIGHT_BLUE (dark) + PAPER_LIGHT palettes — single source of truth
│ ├── images.py # async thumbnail loader (off-thread, on-disk cache)
│ ├── tray.py # optional system-tray icon (pystray, guarded)
│ └── app.py # legacy CustomTkinter application
└── web/
├── server.py # Flask app + JSON API (see Web API)
├── templates/ # Jinja2 — base.html injects the shared palette into Tailwind
└── static/ # css/, icons/, dist/ (compiled TS bundle)
desktop/ # native Tauri desktop shell — spawns web/server.py as a
# sidecar and opens a native window at it (see Desktop Shell)
Everything both UIs need funnels through one object:
-
Fetching:
refresh_now()(sync) /refresh_now_async()(async, used by the desktop GUI so network I/O never blocks the Tk main loop). Both call_apply_fetch(), which upserts into SQLite, detects genuinely new events (vs. a first-ever populate, so it doesn't announce 60 events at once), and optionally fires notifications/webhook/Telegram. -
Queries:
get_events(...)— applies the region filter by default (an event shows if it's Global or its region is selected), plus optional search/type/favorites narrowing. -
Display helpers:
countdown(),description(),format_time()— all localized through the sharedTranslator. -
Settings: typed setters (
set_webhook,set_telegram,set_time_format, ...) that write throughConfigand, where relevant, immediately re-apply to running components (e.g.set_intervalreschedules the live APScheduler job). -
Lifecycle:
start()/stop()— starts the background scheduler and (optionally) an immediate first fetch on a daemon thread.
-
SQLite: one connection per operation (
Database._connect()), never shared across threads — safe for the Tk main thread and the APScheduler background thread to both touch the DB. -
Desktop GUI: all Tk objects are created/touched only on the main
thread. Background work (fetches, image downloads) posts results onto a
queue.Queuethat the main loop drains (_ui_queue) — this is the only handoff point between worker threads and Tk. -
Async: the desktop GUI runs a persistent background asyncio loop
(
core/asyncrunner.py,RUNNERsingleton) soawait-based network I/O (httpx) doesn't need a fresh loop per call; syncDatabasework inside an async path is offloaded viaasyncio.to_thread.
Event (core/models.py) is the single source-agnostic representation.
Parsers (LeekDuckSource, PokemonGoBlogSource) build Event objects;
Database (de)serializes them to/from SQLite rows (highlights — bosses,
promo codes, spawn/research flags — are stored as a JSON blob in one extra
column); both UIs render the same object. Datetimes are always normalized to
timezone-naive local wall-clock time at parse time (_parse_dt) — this
was a real bug fix (mixed naive/aware inputs previously crashed comparisons)
and every downstream comparison depends on that invariant holding.
Translator (i18n.py) loads languages.json once and resolves dotted keys
(events.view_details) with current language → English → the key itself
fallback, so a missing translation degrades visibly instead of crashing.
service.t(key, **kwargs) is the pass-through both UIs call — never a
hard-coded string in a template or widget.
PokéTrack intentionally uses more than one language, each doing what it does best — but Python remains the orchestrator, and both additions are optional with automatic fallback, verified by parity tests.
A PyO3 extension (parse_feed, classify_region) that
parses the ScrapedDuck feed and infers regions in native code, built as an
abi3 wheel (cp39-abi3 — one wheel works on any CPython ≥ 3.9, no
per-interpreter rebuild). poketrack/core/native.py imports it if present;
parser.py's _parse_text() uses it when available and falls back to the
pure-Python path on any import/runtime failure — the app and the full test
suite behave identically either way. Measured on the live feed, the native
JSON→structured-data step is ×ばつ faster; the end-to-end gain is smaller
because building Python Event objects (unavoidable Python work) dominates —
see poketrack-native/benchmark.py for the honest numbers.
The web UI's interactive layer is TypeScript, bundled by
Vite into a committed poketrack/web/static/dist/app.js
— running the app needs no Node, only rebuilding does. It progressively
enhances the Flask server-rendered page (works with JS disabled): live-ticking
countdowns, instant client-side search, no-reload favorite toggling
(POST /api/favorite), and the async refresh/poller.
See Contributing for how to build/test each piece, and Deployment for how CI and releases assemble everything.
Using PokéTrack
Developing