Local-first tool for centralizing contacts, importing race results, and matching the two without depending on external services every time.
After a race with several thousand participants, the goal is to answer one question quickly: which contacts took part, and what was their result? The project is designed from the start as three independent, reusable building blocks:
contacts: import and store contacts locally.race_results: fetch and normalize race results.matching: match already-stored data and produce an actionable table.
A possible future extension is long-document analysis, for example meeting PDFs, to find passages that mention specific contacts.
- Local source of truth: SQLite.
- Secondary exports: JSON/CSV when useful.
- Code organized by reusable domains, not as a single script.
- Local project, no remote backend.
- No ORM by default:
sqlite3is enough.
The first contacts building block now supports multiple local contact sources:
- Desktop OAuth via Google People API (
google_people) - Snapshot import of Google Contacts CSV exports (
google_contacts_csv) - Multiple sources can coexist in the same SQLite database without automatic merging
- Resync and reimport are isolated by
sourceandsource_account - Local inspection without network calls
- JSON export of the local state
Local storage currently relies on three tables:
contactscontact_methodssync_runs
The second race_results building block is now implemented for ACN Timing:
- parsing a public ACN Timing URL
- fetching event metadata through Chronorace
- fetching the results table through the Chronorace API used by ACN
- raw JSON snapshots under
raw/acn_timing/in the configureddata_dir - local SQLite storage in
race_results.sqlite3inside the configureddata_dir - manual dataset aliases so you do not have to rely only on
dataset_id
The third matching building block is now available:
- 100% local matching between
contactsandrace_results - name normalization for accents, casing, and punctuation
- exact matching first, then fuzzy matching with
rapidfuzz - ambiguity guardrails via a minimum score and a minimum gap between candidates
- terminal display and CSV export
- sorting and filtering on matches (
time,team,athlete, etc.) - reusable manual aliases on contacts
- manual reviews to accept or reject a match result by result
The current matching quality is considered good enough for day-to-day use. The product priority is now to keep improving the local GUI, especially around manual review workflows.
python -m venv .venv
source .venv/bin/activate
pip install -e .[gui]Python 3.10+ is supported.
The project now uses a machine-local config file to choose where local state is stored.
Config file:
~/.config/match_my_contacts/config.tomlOn Windows, the equivalent location is:
$env:APPDATA\match_my_contacts\config.tomlThe first CLI or GUI run creates this file automatically if it does not exist yet, pointing to data/ at the project root.
Format:
data_dir = "/absolute/path/to/match_my_contacts_data"
credentials_path = "/absolute/path/to/credentials.json"All local state then derives from that directory:
contacts.sqlite3race_results.sqlite3google/token.jsonraw/acn_timing/exports/
This makes it possible to point several machines to a shared Dropbox-backed folder, as long as only one machine uses those SQLite databases at a time.
To inspect the active config and resolved paths:
match-my-contacts config showTo install the PySide6 desktop GUI as well:
pip install -e .[gui]On Linux with an X11 session, Qt may also require the system library libxcb-cursor0:
sudo apt install libxcb-cursor0- Create a Google Cloud project.
- Enable Google People API.
- Create OAuth credentials for a Desktop application.
- Download the
credentials.jsonfile.
The credentials file can stay outside the repository. The OAuth token generated by the CLI is stored locally under google/token.json in the configured data_dir.
If credentials.json is present at the project root, the sync command uses it automatically even if the CLI is launched from another directory.
Test the CLI:
match-my-contacts helloSynchronize Google contacts into SQLite:
match-my-contacts contacts sync
match-my-contacts contacts sync-google
match-my-contacts contacts sync --credentials /path/to/credentials.jsonImport a Google Contacts CSV export into the local database:
match-my-contacts contacts import-google-csv --csv-path /path/to/google-contacts.csv
match-my-contacts contacts empty-db
match-my-contacts contacts vacuum-dbFetch an ACN Timing results table:
match-my-contacts race-results fetch-acn --url 'https://www.acn-timing.com/?lng=FR#/events/2157220339092161/ctx/20260412_liege/generic/197994_1/home/LIVE1'List local race datasets:
match-my-contacts race-results list-datasets
match-my-contacts race-results add-alias --dataset-id 1 --alias liege-15k-2026
match-my-contacts race-results list-results --dataset liege-15k-2026 --query dupontRun local matching:
match-my-contacts matching run --dataset liege-15k-2026
match-my-contacts matching list --dataset liege-15k-2026 --team TEAMULIEGE --sort time
match-my-contacts matching export-csv --dataset liege-15k-2026 --output data/exports/matches.csvCorrect edge cases:
match-my-contacts contacts list --query noel
match-my-contacts contacts add-alias --contact-id 42 --alias "Jean Noel"
match-my-contacts matching run --dataset liege-15k-2026 --include-ambiguous --limit 20
match-my-contacts matching accept --dataset liege-15k-2026 --result-id 1234 --contact-id 42
match-my-contacts matching reject --dataset liege-15k-2026 --result-id 5678 --note "same name"
match-my-contacts matching list-reviews --dataset liege-15k-2026List local contacts:
match-my-contacts contacts list
match-my-contacts contacts list --query dupont
match-my-contacts contacts list --source google_people
match-my-contacts contacts list-sourcesExport local state as JSON:
match-my-contacts contacts export-json --output data/exports/contacts.jsonRun tests:
pytest -qLaunch the local GUI:
match-my-contacts-guiThe current GUI is intentionally simple, but already useful for daily work:
- local desktop UI in PySide6
Contacts,Race Results,Matching, andConfigtabsHelpmenu withAboutandCredits- single central table
- Google sync from the
Contactstab - summary dialog after
Sync Google - local contacts auto-load on startup when the database already exists
- CSV import targeted at the real Google Contacts export format
Empty DB...button with explicit confirmationVACUUM DBbutton to compactcontacts.sqlite3when needed- visible-column selection for the contacts table
- optional source visibility in the contacts table
- detailed contact dialog on double-click with source metadata
- ACN import from the GUI
- dataset alias creation
- contacts JSON export
- local matching filters and CSV export
- local config inspection and editing
- no network auto-sync on startup
- manual reviews still remain CLI-only for now
- run the CLI or GUI once to create the local config file
- edit
config.tomlsodata_dirpoints to a shared Dropbox folder - copy the current contents of
data/into that shared folder - restart
match-my-contactsormatch-my-contacts-gui - verify that contacts, datasets, aliases, and exports are all present
Precautions:
- do not open the same SQLite database on two machines at the same time
- let Dropbox finish syncing before switching machines
- if Dropbox creates conflict copies, inspect the
.sqlite3files and conflicted copies before continuing
Practical usage guide:
- read
USAGE.md
Resume file for a future Codex session:
- read
HANDOFF.md
Recent GUI updates:
- local contacts auto-load on startup when the database already exists
Sync Googlebutton in theContactstab- summary dialog after
Sync Google Empty DB...button with confirmation and matching-review purgeVACUUM DBbutton to compact the local database- CSV import targeted at the real Google Contacts export format
- persistent visible-column selection in the contacts table
- optional source column in the contacts table
- detailed contact dialog on double-click with source metadata
Helpmenu withAboutandCredits
- Keep consolidating the GUI as the daily local control surface.
- Add manual match review to the GUI next.
- Keep the business logic and CLI stable while the GUI grows.
- Extend
race_resultsto other providers if needed.