thistle-db 0.6.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.
@@ -0,0 +1,133 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+ __about__.py
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py,cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ .python-version
87
+
88
+ # pipenv
89
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
90
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
91
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
92
+ # install all needed dependencies.
93
+ #Pipfile.lock
94
+
95
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
96
+ __pypackages__/
97
+
98
+ # Celery stuff
99
+ celerybeat-schedule
100
+ celerybeat.pid
101
+
102
+ # SageMath parsed files
103
+ *.sage.py
104
+
105
+ # Environments
106
+ .env
107
+ .venv
108
+ env/
109
+ venv/
110
+ ENV/
111
+ env.bak/
112
+ venv.bak/
113
+
114
+ # Spyder project settings
115
+ .spyderproject
116
+ .spyproject
117
+
118
+ # Rope project settings
119
+ .ropeproject
120
+
121
+ # mkdocs documentation
122
+ /site
123
+
124
+ # mypy
125
+ .mypy_cache/
126
+ .dmypy.json
127
+ dmypy.json
128
+
129
+ # Pyre type checker
130
+ .pyre/
131
+
132
+ # hatch-vcs
133
+ _version.py
@@ -0,0 +1,148 @@
1
+ # CLAUDE.md
2
+
3
+ This file is the **target design spec** for thistle-db. Where the code and this
4
+ document disagree, this document wins — update the code to match, not the other
5
+ way around. Sections marked **[NOT YET IMPLEMENTED]** are known gaps.
6
+
7
+ ## What thistle-db is
8
+
9
+ thistle-db is a database manager for orbital element sets. It ingests TLE
10
+ (Two-Line Element) and OMM (Orbit Mean-Elements Message) files from configured
11
+ directories into a SQL database with uniqueness guarantees, and generates
12
+ organized output files (by date and by satellite) from that database.
13
+
14
+ ## Core workflow
15
+
16
+ 1. TLE/OMM files are delivered into directories defined in `config.toml`
17
+ (`[[ingest.sources]]` entries: a `path` plus a glob `pattern`).
18
+ 2. `thistle-db ingest` scans those directories, auto-detects each file's format,
19
+ parses the element sets, and inserts them into the database. Duplicates are
20
+ silently skipped — ingest is **idempotent**: running it twice over the same
21
+ files must never create duplicate rows or fail.
22
+ 3. `thistle-db generate` writes output files from the database.
23
+
24
+ ### File formats
25
+
26
+ Format is auto-detected per file (`reader.detect_format`):
27
+
28
+ | Format | Extensions | Reader |
29
+ |---|---|---|
30
+ | TLE (2-line or 3-line) | `.tle`, `.txt`, `.3le` | `read_tle` |
31
+ | OMM JSON (Space-Track) | `.json` | `read_omm_json` |
32
+ | OMM CSV | `.csv` | `read_omm_csv` |
33
+ | OMM XML | `.xml` | `read_omm_xml` |
34
+
35
+ ### Delivery patterns to handle
36
+
37
+ - **New file per delivery** (e.g. daily `YYYYMMDD.txt`): straightforward scan-and-ingest.
38
+ - **In-place updates**: a provider may append to or rewrite an existing file
39
+ rather than delivering a delta. This must be handled elegantly:
40
+ - Correctness comes from DB-level dedup — re-reading a whole updated file and
41
+ inserting only the new element sets is always safe.
42
+ - Efficiency comes from **file-state tracking**: each successfully ingested
43
+ file's path, size, mtime, and content hash is recorded in the
44
+ `ingest_files` table (`IngestFile` model). On scan
45
+ (`ingest.ingest_source_file`), unchanged files (size + mtime match) are
46
+ skipped without being opened; a changed file whose content hash still
47
+ matches just refreshes its state; otherwise the file is re-ingested (dedup
48
+ absorbs the already-seen records). A file that fails to parse gets no
49
+ state recorded, so it is retried next scan. `--force` bypasses skipping.
50
+
51
+ ## Data model (`model.py`)
52
+
53
+ Single canonical element-set table plus an OMM metadata sidecar. Do **not**
54
+ split OMM into a fully separate element-set table.
55
+
56
+ - **`tle`** — one row per unique element set. Stores the raw `line1`/`line2`
57
+ text plus parsed fields (norad_cat_id, epoch, Keplerian elements, drag terms)
58
+ and derived values (semimajor axis, period, apoapsis/periapsis altitude).
59
+ Parsing goes through `sgp4.Satrec`.
60
+ - **`omm_metadata`** — OMM-only fields (object name/type, country code, RCS
61
+ size, launch/decay date, site, originator, GP_ID), one-to-one with a `tle`
62
+ row via `tle_id`.
63
+ - **`ingest_files`** — per-file ingest state (path, size, mtime_ns, sha256)
64
+ used to skip unchanged source files on scan; keyed by a sha256 of the
65
+ resolved path (bounded length for cross-dialect unique indexes).
66
+
67
+ ### Uniqueness
68
+
69
+ The dedup key is **exact text of `(line1, line2)`** (a `UniqueConstraint`).
70
+ This is deliberate: it is lossless and simple; textual near-duplicates from
71
+ different providers coexist as separate rows. Do not change the key to
72
+ `(norad_cat_id, epoch)` or similar without an explicit decision.
73
+
74
+ If a TLE is delivered first and the OMM version of the same element set arrives
75
+ later (with identical lines), the TLE row is **not** duplicated — the OMM
76
+ delivery attaches an `omm_metadata` row to the existing `tle` row. Both
77
+ representations are thereby preserved: the TLE lines in `tle`, the OMM extras
78
+ in `omm_metadata`.
79
+
80
+ Dedup is enforced at the database with dialect-aware upserts
81
+ (`ingest._bulk_insert_tles`): `ON CONFLICT DO NOTHING` for SQLite/PostgreSQL,
82
+ `INSERT IGNORE` for MySQL/MariaDB. Inserts are chunked (`CHUNK_SIZE = 5000`).
83
+ Any new bulk-write path must follow this pattern and work on all three dialects.
84
+
85
+ ## Configuration (`config.py`)
86
+
87
+ - `config.toml` (path via `-c`, default `./config.toml`), parsed into
88
+ pydantic-settings models: `[database]`, `[[ingest.sources]]`, `[output]`,
89
+ `[logging]`.
90
+ - Database credentials are **never** stored in `config.toml`. Resolution order
91
+ (highest priority first):
92
+ 1. Env vars `THISTLE_DB_DATABASE__USERNAME` / `THISTLE_DB_DATABASE__PASSWORD`
93
+ 2. User secrets file `~/.config/thistle-db.toml`
94
+ 3. System secrets file (`database.secrets_file` in config)
95
+ - Supported databases: SQLite (default/dev), MariaDB/MySQL (operational,
96
+ `thistle-db[mysql]` extra), PostgreSQL (tested in CI).
97
+
98
+ ## CLI (`cli.py`)
99
+
100
+ `thistle-db [-c CONFIG] <command>` — the config path defaults to the
101
+ `THISTLE_DB_CONFIG` env var when set (shared with thistle's db fallback),
102
+ else `./config.toml`:
103
+
104
+ - `init` — scaffold `config.toml` and `~/.config/thistle-db.toml`.
105
+ - `ingest [FILES...] [--force]` — ingest specific files (always parsed, even
106
+ if recorded as unchanged), or scan all configured sources when no files are
107
+ given; `--force` re-ingests scanned files regardless of recorded state.
108
+ - `get-tle TARGET [--days N]` — print TLEs to stdout. `TARGET` is either an
109
+ 8-digit date `YYYYMMDD` (→ the nearest TLE per object to 12:00 UTC on that
110
+ date, within ±N days, default 7) or an alpha-5-compatible NORAD ID
111
+ (e.g. `25544`, `00022`, `E5693` → every TLE for that object, epoch order).
112
+ Exits 1 if nothing matches.
113
+ - `generate` — write output files per `[output]` config:
114
+ - `date_files`: `YYYYMMDD.{tle,omm}` — latest element set per satellite for
115
+ that date.
116
+ - `object_files`: `NORAD_ID.{tle,omm}` — all element sets for a satellite,
117
+ ordered by epoch.
118
+ - Formats toggleable via `[output.formats]` (tle = two-line text, omm = CSV).
119
+
120
+ ## Module map
121
+
122
+ | Module | Responsibility |
123
+ |---|---|
124
+ | `cli.py` | typer CLI, subcommand dispatch, logging setup |
125
+ | `config.py` | pydantic-settings config + layered secrets resolution |
126
+ | `model.py` | SQLAlchemy ORM models (`TLE`, `OmmMetadata`), TLE parsing via sgp4 |
127
+ | `reader.py` | format detection + file readers (TLE, OMM JSON/CSV/XML) |
128
+ | `ingest.py` | bulk insert with dedup, source-directory scanning |
129
+ | `generator.py` | output file generation from the database |
130
+
131
+ ## Error handling philosophy
132
+
133
+ Ingest is tolerant: a malformed TLE or OMM record is logged (loguru, WARNING)
134
+ and skipped — one bad record must never abort a file, and one bad file must
135
+ never abort a scan. A missing source directory is a warning, not an error.
136
+
137
+ ## Development
138
+
139
+ - Python ≥ 3.11. Package manager: `uv`. Build: hatchling + hatch-vcs
140
+ (version comes from git tags — never hardcode it).
141
+ - Run tests: `uv run pytest tests/thistle_db` from the workspace root. Lint:
142
+ `uv run ruff check`. Types: `uv run pyright`.
143
+ - Tests cover SQLite by default; set `THISTLE_DB_TEST_MARIADB=1` /
144
+ `THISTLE_DB_TEST_POSTGRES=1` (requires Docker) to run the MariaDB and
145
+ PostgreSQL backends via testcontainers — CI runs all three. Dialect-specific
146
+ behavior (upserts) must be tested against all three.
147
+ - Test fixtures live in `tests/thistle_db/data/` (real TLE text files and
148
+ Space-Track OMM JSON).
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Jonathan Olsten
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,269 @@
1
+ Metadata-Version: 2.4
2
+ Name: thistle-db
3
+ Version: 0.6.0
4
+ Summary: Orbital element database management tool
5
+ Author-email: Jonathan Olsten <jolsten@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: loguru>=0.7.3
10
+ Requires-Dist: pydantic-settings>=2.9.1
11
+ Requires-Dist: sgp4>=2.24
12
+ Requires-Dist: sqlalchemy>=2.0.40
13
+ Requires-Dist: typer>=0.27.0
14
+ Provides-Extra: mysql
15
+ Requires-Dist: pymysql>=1.1; extra == 'mysql'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # thistle-db
19
+
20
+ Orbital element database manager. Ingests TLE (Two-Line Element) and OMM (Orbit Mean-Elements Message) files into a database and generates organized output files by date and satellite.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install thistle-db
26
+
27
+ # For MariaDB/MySQL support:
28
+ pip install thistle-db[mysql]
29
+ ```
30
+
31
+ Or with [uv](https://docs.astral.sh/uv/):
32
+
33
+ ```bash
34
+ uv add thistle-db
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ### 1. Scaffold configuration
40
+
41
+ ```bash
42
+ thistle-db init
43
+ ```
44
+
45
+ This creates two files:
46
+
47
+ - `./config.toml` -- main configuration (database, ingest sources, output settings)
48
+ - `~/.config/thistle-db.toml` -- user-local database credentials
49
+
50
+ Use `-c` to specify a different config path:
51
+
52
+ ```bash
53
+ thistle-db -c /etc/thistle-db/config.toml init
54
+ ```
55
+
56
+ ### 2. Configure
57
+
58
+ Edit `config.toml` to set your database and ingest sources. The generated file is fully commented -- see below for a summary.
59
+
60
+ **SQLite (default):**
61
+
62
+ ```toml
63
+ [database]
64
+ drivername = "sqlite"
65
+ name = "thistle-db.db"
66
+ ```
67
+
68
+ **MariaDB/MySQL:**
69
+
70
+ ```toml
71
+ [database]
72
+ drivername = "mysql+pymysql"
73
+ host = "localhost"
74
+ port = 3306
75
+ name = "thistle-db"
76
+ secrets_file = "/etc/thistle-db/secrets.toml"
77
+ ```
78
+
79
+ Then add your credentials to `~/.config/thistle-db.toml`:
80
+
81
+ ```toml
82
+ username = "myuser"
83
+ password = "mypassword"
84
+ ```
85
+
86
+ ### 3. Ingest TLE/OMM files
87
+
88
+ Scan configured source directories:
89
+
90
+ ```bash
91
+ thistle-db ingest
92
+ ```
93
+
94
+ Or ingest specific files:
95
+
96
+ ```bash
97
+ thistle-db ingest /path/to/20260327.tle /path/to/20260327.json
98
+ ```
99
+
100
+ File format is auto-detected by extension:
101
+
102
+ | Extension | Format |
103
+ | ------------------ | ------------------- |
104
+ | `.tle`, `.txt`, `.3le` | Two-Line Element |
105
+ | `.json` | Space-Track OMM JSON |
106
+ | `.csv` | OMM CSV |
107
+ | `.xml` | OMM XML |
108
+
109
+ Ingestion is idempotent -- duplicate records are silently skipped.
110
+
111
+ ### 4. Generate output files
112
+
113
+ ```bash
114
+ thistle-db generate
115
+ ```
116
+
117
+ This produces files in the configured output directory:
118
+
119
+ - **Date files** (`YYYYMMDD.tle` / `YYYYMMDD.omm`) -- one TLE per satellite for each date (latest epoch that day)
120
+ - **Object files** (`25544.tle` / `25544.omm`) -- all TLEs for a single satellite, ordered by epoch
121
+
122
+ ## Automating with Cron
123
+
124
+ thistle-db is designed to run via cron rather than as a long-running service. Both `ingest` and `generate` are idempotent and safe to re-run.
125
+
126
+ **Ingest and generate every 4 hours:**
127
+
128
+ ```cron
129
+ 0 */4 * * * thistle-db -c /etc/thistle-db/config.toml ingest && thistle-db -c /etc/thistle-db/config.toml generate
130
+ ```
131
+
132
+ **Ingest hourly, generate once daily at 03:00 UTC:**
133
+
134
+ ```cron
135
+ 0 * * * * thistle-db -c /etc/thistle-db/config.toml ingest
136
+ 0 3 * * * thistle-db -c /etc/thistle-db/config.toml generate
137
+ ```
138
+
139
+ **With logging to a file:**
140
+
141
+ ```cron
142
+ 0 */4 * * * thistle-db -c /etc/thistle-db/config.toml ingest >> /var/log/thistle-db.log 2>&1 && thistle-db -c /etc/thistle-db/config.toml generate >> /var/log/thistle-db.log 2>&1
143
+ ```
144
+
145
+ ## Credential Resolution
146
+
147
+ Database credentials are resolved in priority order:
148
+
149
+ 1. **Environment variables** -- `THISTLE_DB_DATABASE__USERNAME` / `THISTLE_DB_DATABASE__PASSWORD`
150
+ 2. **User secrets file** -- `~/.config/thistle-db.toml`
151
+ 3. **System secrets file** -- path set via `secrets_file` in `config.toml`
152
+ 4. **config.toml values** -- not recommended for credentials
153
+
154
+ For cron jobs, either use the user secrets file or export environment variables in the crontab:
155
+
156
+ ```cron
157
+ THISTLE_DB_DATABASE__USERNAME=myuser
158
+ THISTLE_DB_DATABASE__PASSWORD=mypassword
159
+ 0 */4 * * * thistle-db -c /etc/thistle-db/config.toml ingest && thistle-db -c /etc/thistle-db/config.toml generate
160
+ ```
161
+
162
+ ## CLI Reference
163
+
164
+ ```
165
+ thistle-db [-c CONFIG] COMMAND
166
+
167
+ Commands:
168
+ init Scaffold config.toml and ~/.config/thistle-db.toml
169
+ ingest Ingest TLE/OMM files into the database
170
+ generate Generate output TLE/OMM files from the database
171
+ get-tle Print TLEs from the database to stdout
172
+
173
+ Options:
174
+ -c, --config PATH Path to config.toml
175
+ (default: $THISTLE_DB_CONFIG if set, else ./config.toml)
176
+ ```
177
+
178
+ ### `get-tle`
179
+
180
+ Query the database directly and print TLEs to stdout. The positional argument
181
+ is either a NORAD ID (alpha-5 compatible, e.g. `25544`, `00022`, `E5693`) or
182
+ an 8-digit date (`YYYYMMDD`):
183
+
184
+ ```bash
185
+ # All TLEs for one satellite, ordered by epoch
186
+ thistle-db get-tle 25544
187
+ thistle-db get-tle E5693 # alpha-5 IDs work too (= 145693)
188
+
189
+ # Nearest TLE per satellite to 12:00 UTC on a date, within +/- 7 days
190
+ thistle-db get-tle 20260717
191
+
192
+ # Widen (or narrow) the search window
193
+ thistle-db get-tle 20260717 --days 3
194
+ ```
195
+
196
+ Exits with status 1 if no TLEs match.
197
+
198
+ ## Configuration Reference
199
+
200
+ ### `[database]`
201
+
202
+ | Field | Default | Description |
203
+ | -------------- | ------------ | ------------------------------------------ |
204
+ | `drivername` | `"sqlite"` | SQLAlchemy driver (`sqlite`, `mysql+pymysql`) |
205
+ | `name` | `":memory:"` | Database name or file path |
206
+ | `host` | | Database host |
207
+ | `port` | | Database port |
208
+ | `username` | | Database username (prefer secrets file) |
209
+ | `password` | | Database password (prefer secrets file) |
210
+ | `secrets_file` | | Path to a TOML file with username/password |
211
+
212
+ ### `[[ingest.sources]]`
213
+
214
+ | Field | Default | Description |
215
+ | --------- | --------- | ---------------------------------- |
216
+ | `path` | | Directory to scan for files |
217
+ | `pattern` | `"*.tle"` | Glob pattern for matching files |
218
+
219
+ ### `[output]`
220
+
221
+ | Field | Default | Description |
222
+ | ------- | ------------ | ------------------------------- |
223
+ | `dir` | `"./output"` | Output directory |
224
+
225
+ ### `[output.formats]`
226
+
227
+ | Field | Default | Description |
228
+ | ----- | ------- | --------------------------- |
229
+ | `tle` | `true` | Generate .tle output files |
230
+ | `omm` | `true` | Generate .omm (CSV) output |
231
+
232
+ ### `[output.types]`
233
+
234
+ | Field | Default | Description |
235
+ | -------------- | ------- | -------------------------------------------------- |
236
+ | `date_files` | `true` | YYYYMMDD files with latest TLE per satellite |
237
+ | `object_files` | `true` | Per-satellite files with all TLEs ordered by epoch |
238
+
239
+ ### `[logging]`
240
+
241
+ | Field | Default | Description |
242
+ | ------- | -------- | ---------------------------------------------- |
243
+ | `level` | `"INFO"` | Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL |
244
+
245
+ ## Development
246
+
247
+ ### Running tests
248
+
249
+ Tests live at the workspace root under `tests/thistle_db/` and are
250
+ parametrized to run against SQLite, MariaDB, and PostgreSQL. SQLite runs
251
+ unconditionally; the MariaDB and PostgreSQL backends are opt-in and managed
252
+ automatically by [testcontainers](https://testcontainers-python.readthedocs.io/)
253
+ — one container per test session, one throwaway database per test. No manual
254
+ `docker run` needed, just a running Docker daemon.
255
+
256
+ SQLite only:
257
+
258
+ ```bash
259
+ uv run pytest tests/thistle_db
260
+ ```
261
+
262
+ All backends (requires Docker):
263
+
264
+ ```bash
265
+ THISTLE_DB_TEST_MARIADB=1 THISTLE_DB_TEST_POSTGRES=1 uv run pytest tests/thistle_db
266
+ ```
267
+
268
+ The images default to `mariadb:11` and `postgres:16`; override with
269
+ `THISTLE_DB_MARIADB_IMAGE` / `THISTLE_DB_POSTGRES_IMAGE`.