Skip to content

Navigation Menu

Sign in
Sign up

Architecture

chin52696411 edited this page Jul 25, 2026 · 2 revisions

Architecture

Layering

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 spawns poketrack.web's Flask server as a sidecar process and opens a native window pointed at it, so it reuses poketrack.web entirely 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)

Module map

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)

PokeTrackService — the shared controller

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 shared Translator.
  • Settings: typed setters (set_webhook, set_telegram, set_time_format, ...) that write through Config and, where relevant, immediately re-apply to running components (e.g. set_interval reschedules the live APScheduler job).
  • Lifecycle: start()/stop() — starts the background scheduler and (optionally) an immediate first fetch on a daemon thread.

Threading model

  • 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.Queue that 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, RUNNER singleton) so await-based network I/O (httpx) doesn't need a fresh loop per call; sync Database work inside an async path is offloaded via asyncio.to_thread.

Data model

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.

i18n

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.

Polyglot design

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.

Rust native fast path (poketrack-native/)

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.

TypeScript web front-end (web-frontend/)

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.

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /