bottomtime 0.1.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.
- bottomtime-0.1.0/.github/workflows/test.yml +20 -0
- bottomtime-0.1.0/.gitignore +10 -0
- bottomtime-0.1.0/LICENSE +21 -0
- bottomtime-0.1.0/PKG-INFO +108 -0
- bottomtime-0.1.0/README.md +88 -0
- bottomtime-0.1.0/pyproject.toml +34 -0
- bottomtime-0.1.0/src/bottomtime/__init__.py +7 -0
- bottomtime-0.1.0/src/bottomtime/api.py +88 -0
- bottomtime-0.1.0/src/bottomtime/cli.py +107 -0
- bottomtime-0.1.0/src/bottomtime/decode/__init__.py +1 -0
- bottomtime-0.1.0/src/bottomtime/decode/pnf.py +435 -0
- bottomtime-0.1.0/src/bottomtime/ingest/__init__.py +1 -0
- bottomtime-0.1.0/src/bottomtime/ingest/archive.py +48 -0
- bottomtime-0.1.0/src/bottomtime/ingest/garmin_fit.py +303 -0
- bottomtime-0.1.0/src/bottomtime/ingest/shearwater_db.py +231 -0
- bottomtime-0.1.0/src/bottomtime/ingest/shearwater_xml.py +92 -0
- bottomtime-0.1.0/src/bottomtime/match/__init__.py +1 -0
- bottomtime-0.1.0/src/bottomtime/match/correlate.py +95 -0
- bottomtime-0.1.0/src/bottomtime/match/matcher.py +334 -0
- bottomtime-0.1.0/src/bottomtime/store/__init__.py +1 -0
- bottomtime-0.1.0/src/bottomtime/store/db.py +37 -0
- bottomtime-0.1.0/src/bottomtime/store/schema.py +209 -0
- bottomtime-0.1.0/src/bottomtime/verify/__init__.py +1 -0
- bottomtime-0.1.0/src/bottomtime/verify/crosscheck.py +229 -0
- bottomtime-0.1.0/tests/test_correlate.py +39 -0
- bottomtime-0.1.0/tests/test_pnf.py +148 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.14"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python-version }}
|
|
19
|
+
- run: pip install -e .[dev]
|
|
20
|
+
- run: pytest -q
|
bottomtime-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Snehan Kekre
|
|
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.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bottomtime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lossless dive-log store: ingest, decode, and reconcile dives from Garmin (FIT) and Shearwater (Petrel Native Format) dive computers
|
|
5
|
+
Project-URL: Homepage, https://github.com/snehankekre/bottomtime
|
|
6
|
+
Project-URL: Issues, https://github.com/snehankekre/bottomtime/issues
|
|
7
|
+
Author: Snehan Kekre
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: decompression,dive-log,diving,fit,garmin,scuba,shearwater,uddf
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: garmin-fit-sdk>=21.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# bottomtime
|
|
22
|
+
|
|
23
|
+
A lossless dive-log store for divers who log with more than one computer.
|
|
24
|
+
|
|
25
|
+
Tec divers often wear two dive computers (say, a Garmin Descent on one wrist
|
|
26
|
+
and a Shearwater on the other). Each records the same dive differently: the
|
|
27
|
+
Garmin logs at 1 Hz with heart rate, GPS, CNS and tissue loading; the
|
|
28
|
+
Shearwater logs its native record with GF99, deco ceiling, ppO2 sensors and
|
|
29
|
+
per-sample battery. No existing interchange format (UDDF, DL7, Subsurface)
|
|
30
|
+
preserves the union, so exporting to any of them throws data away.
|
|
31
|
+
|
|
32
|
+
bottomtime keeps everything:
|
|
33
|
+
|
|
34
|
+
- **Ingests Garmin FIT files** (official garmin-fit-sdk). Known dive channels
|
|
35
|
+
become typed columns; unknown fields and Garmin's undocumented message
|
|
36
|
+
types are preserved raw, per sample and per message.
|
|
37
|
+
- **Decodes Shearwater's Petrel Native Format** directly from a Shearwater
|
|
38
|
+
Cloud database (`dive_data.db`, the "Export Database" output) with a pure
|
|
39
|
+
Python decoder — including per-sample **GF99, deco ceiling, CNS and battery
|
|
40
|
+
voltage** that Shearwater's own XML/CSV exports omit. Unknown record types
|
|
41
|
+
are preserved raw.
|
|
42
|
+
- **Archives every source file verbatim**, content-addressed by SHA-256. The
|
|
43
|
+
database is a decoded view; the archive is the source of truth.
|
|
44
|
+
- **Reconciles dual-computer dives**: interval overlap plus depth-profile
|
|
45
|
+
cross-correlation links the two logs of the same physical dive (storing
|
|
46
|
+
clock offset and residual skew) without ever merging or resampling the
|
|
47
|
+
original series.
|
|
48
|
+
- **Verifies itself**: the decoder is cross-checked per-sample against
|
|
49
|
+
Shearwater XML exports, against Shearwater Cloud's own computed values
|
|
50
|
+
(EndGF99), and matched dives are checked for depth agreement.
|
|
51
|
+
|
|
52
|
+
Everything lands in a single SQLite file with a stable schema, ready for SQL,
|
|
53
|
+
pandas, or whatever you analyze with.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
pip install bottomtime
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
# create a store
|
|
65
|
+
bottomtime init
|
|
66
|
+
|
|
67
|
+
# ingest a directory of Garmin FIT files
|
|
68
|
+
# (optionally with a Garmin Connect index JSON for true UTC offsets and metadata)
|
|
69
|
+
bottomtime ingest garmin ~/dives/garmin-fits
|
|
70
|
+
|
|
71
|
+
# ingest a Shearwater Cloud database export
|
|
72
|
+
bottomtime ingest shearwater ~/dives/dive_data.db
|
|
73
|
+
|
|
74
|
+
# link dual-computer dives and build the canonical dive list
|
|
75
|
+
bottomtime match
|
|
76
|
+
|
|
77
|
+
# run the verification suite (XML dir optional but recommended)
|
|
78
|
+
bottomtime verify --xml-dir ~/dives/shearwater-xml
|
|
79
|
+
|
|
80
|
+
bottomtime status
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
All commands are idempotent: re-running an ingest skips already-stored dives,
|
|
84
|
+
so syncing after a dive trip only adds what's new.
|
|
85
|
+
|
|
86
|
+
## Schema in one breath
|
|
87
|
+
|
|
88
|
+
`raw_artifacts` (archived files) → `source_dives` (one row per computer log,
|
|
89
|
+
with the verbatim header and, for Shearwater, the raw PNF blob) →
|
|
90
|
+
`garmin_samples` / `shearwater_samples` (wide, per-source, never resampled) +
|
|
91
|
+
`gases`, `gas_segments`, `events`, `undecoded_payloads` (raw bytes of
|
|
92
|
+
anything not yet understood) → `matches` (dual-computer links) → `dives` +
|
|
93
|
+
`dive_members` (canonical dives). Views `v_dive_summary` and
|
|
94
|
+
`v_samples_unified` cover the common queries.
|
|
95
|
+
|
|
96
|
+
## Notes on the PNF decoder
|
|
97
|
+
|
|
98
|
+
The decoder follows libdivecomputer's `shearwater_predator_parser.c` for the
|
|
99
|
+
documented fields and adds empirically verified mappings for GF99 (byte 25),
|
|
100
|
+
deco ceiling (byte 24), battery voltage (byte 18) and @+5 TTS (bytes 26-27),
|
|
101
|
+
validated against Shearwater Cloud's displayed values across hundreds of
|
|
102
|
+
dives. Bytes without a known meaning are preserved per sample. If your dives
|
|
103
|
+
disagree, `bottomtime verify` will say so loudly — issue reports with a
|
|
104
|
+
failing blob are very welcome.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# bottomtime
|
|
2
|
+
|
|
3
|
+
A lossless dive-log store for divers who log with more than one computer.
|
|
4
|
+
|
|
5
|
+
Tec divers often wear two dive computers (say, a Garmin Descent on one wrist
|
|
6
|
+
and a Shearwater on the other). Each records the same dive differently: the
|
|
7
|
+
Garmin logs at 1 Hz with heart rate, GPS, CNS and tissue loading; the
|
|
8
|
+
Shearwater logs its native record with GF99, deco ceiling, ppO2 sensors and
|
|
9
|
+
per-sample battery. No existing interchange format (UDDF, DL7, Subsurface)
|
|
10
|
+
preserves the union, so exporting to any of them throws data away.
|
|
11
|
+
|
|
12
|
+
bottomtime keeps everything:
|
|
13
|
+
|
|
14
|
+
- **Ingests Garmin FIT files** (official garmin-fit-sdk). Known dive channels
|
|
15
|
+
become typed columns; unknown fields and Garmin's undocumented message
|
|
16
|
+
types are preserved raw, per sample and per message.
|
|
17
|
+
- **Decodes Shearwater's Petrel Native Format** directly from a Shearwater
|
|
18
|
+
Cloud database (`dive_data.db`, the "Export Database" output) with a pure
|
|
19
|
+
Python decoder — including per-sample **GF99, deco ceiling, CNS and battery
|
|
20
|
+
voltage** that Shearwater's own XML/CSV exports omit. Unknown record types
|
|
21
|
+
are preserved raw.
|
|
22
|
+
- **Archives every source file verbatim**, content-addressed by SHA-256. The
|
|
23
|
+
database is a decoded view; the archive is the source of truth.
|
|
24
|
+
- **Reconciles dual-computer dives**: interval overlap plus depth-profile
|
|
25
|
+
cross-correlation links the two logs of the same physical dive (storing
|
|
26
|
+
clock offset and residual skew) without ever merging or resampling the
|
|
27
|
+
original series.
|
|
28
|
+
- **Verifies itself**: the decoder is cross-checked per-sample against
|
|
29
|
+
Shearwater XML exports, against Shearwater Cloud's own computed values
|
|
30
|
+
(EndGF99), and matched dives are checked for depth agreement.
|
|
31
|
+
|
|
32
|
+
Everything lands in a single SQLite file with a stable schema, ready for SQL,
|
|
33
|
+
pandas, or whatever you analyze with.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
pip install bottomtime
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
# create a store
|
|
45
|
+
bottomtime init
|
|
46
|
+
|
|
47
|
+
# ingest a directory of Garmin FIT files
|
|
48
|
+
# (optionally with a Garmin Connect index JSON for true UTC offsets and metadata)
|
|
49
|
+
bottomtime ingest garmin ~/dives/garmin-fits
|
|
50
|
+
|
|
51
|
+
# ingest a Shearwater Cloud database export
|
|
52
|
+
bottomtime ingest shearwater ~/dives/dive_data.db
|
|
53
|
+
|
|
54
|
+
# link dual-computer dives and build the canonical dive list
|
|
55
|
+
bottomtime match
|
|
56
|
+
|
|
57
|
+
# run the verification suite (XML dir optional but recommended)
|
|
58
|
+
bottomtime verify --xml-dir ~/dives/shearwater-xml
|
|
59
|
+
|
|
60
|
+
bottomtime status
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
All commands are idempotent: re-running an ingest skips already-stored dives,
|
|
64
|
+
so syncing after a dive trip only adds what's new.
|
|
65
|
+
|
|
66
|
+
## Schema in one breath
|
|
67
|
+
|
|
68
|
+
`raw_artifacts` (archived files) → `source_dives` (one row per computer log,
|
|
69
|
+
with the verbatim header and, for Shearwater, the raw PNF blob) →
|
|
70
|
+
`garmin_samples` / `shearwater_samples` (wide, per-source, never resampled) +
|
|
71
|
+
`gases`, `gas_segments`, `events`, `undecoded_payloads` (raw bytes of
|
|
72
|
+
anything not yet understood) → `matches` (dual-computer links) → `dives` +
|
|
73
|
+
`dive_members` (canonical dives). Views `v_dive_summary` and
|
|
74
|
+
`v_samples_unified` cover the common queries.
|
|
75
|
+
|
|
76
|
+
## Notes on the PNF decoder
|
|
77
|
+
|
|
78
|
+
The decoder follows libdivecomputer's `shearwater_predator_parser.c` for the
|
|
79
|
+
documented fields and adds empirically verified mappings for GF99 (byte 25),
|
|
80
|
+
deco ceiling (byte 24), battery voltage (byte 18) and @+5 TTS (bytes 26-27),
|
|
81
|
+
validated against Shearwater Cloud's displayed values across hundreds of
|
|
82
|
+
dives. Bytes without a known meaning are preserved per sample. If your dives
|
|
83
|
+
disagree, `bottomtime verify` will say so loudly — issue reports with a
|
|
84
|
+
failing blob are very welcome.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bottomtime"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lossless dive-log store: ingest, decode, and reconcile dives from Garmin (FIT) and Shearwater (Petrel Native Format) dive computers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Snehan Kekre" }]
|
|
14
|
+
keywords = ["diving", "scuba", "dive-log", "garmin", "shearwater", "fit", "uddf", "decompression"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: End Users/Desktop",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["garmin-fit-sdk>=21.0"]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest"]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
bottomtime = "bottomtime.cli:main"
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/snehankekre/bottomtime"
|
|
31
|
+
Issues = "https://github.com/snehankekre/bottomtime/issues"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/bottomtime"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Small read API for analysis code.
|
|
2
|
+
|
|
3
|
+
import bottomtime
|
|
4
|
+
dive = bottomtime.load_dive("data/dives.db", 291)
|
|
5
|
+
dive["sources"]["garmin"]["samples"]["depth_m"]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sqlite3
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_SAMPLE_TABLES = {"garmin": "garmin_samples", "shearwater": "shearwater_samples"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _columns_of(con: sqlite3.Connection, table: str) -> list[str]:
|
|
18
|
+
return [r[1] for r in con.execute(f"PRAGMA table_info({table})")]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_dive(db_path: str | Path, dive_number: int) -> dict:
|
|
22
|
+
"""Load one canonical dive with every member source log's samples as
|
|
23
|
+
column-oriented lists (ready for plotting / DataFrame construction)."""
|
|
24
|
+
con = sqlite3.connect(db_path)
|
|
25
|
+
con.row_factory = sqlite3.Row
|
|
26
|
+
try:
|
|
27
|
+
dive = con.execute(
|
|
28
|
+
"SELECT * FROM dives WHERE dive_number=?", (dive_number,)
|
|
29
|
+
).fetchone()
|
|
30
|
+
if dive is None:
|
|
31
|
+
raise KeyError(f"no dive with number {dive_number}")
|
|
32
|
+
out = {k: dive[k] for k in dive.keys()}
|
|
33
|
+
out["sources"] = {}
|
|
34
|
+
members = con.execute(
|
|
35
|
+
"SELECT sd.* FROM source_dives sd JOIN dive_members dm"
|
|
36
|
+
" ON dm.source_dive_id = sd.id WHERE dm.dive_id=?",
|
|
37
|
+
(dive["id"],),
|
|
38
|
+
).fetchall()
|
|
39
|
+
for sd in members:
|
|
40
|
+
table = _SAMPLE_TABLES[sd["source"]]
|
|
41
|
+
cols = [c for c in _columns_of(con, table) if c != "source_dive_id"]
|
|
42
|
+
rows = con.execute(
|
|
43
|
+
f"SELECT {', '.join(cols)} FROM {table}"
|
|
44
|
+
" WHERE source_dive_id=? ORDER BY t_s",
|
|
45
|
+
(sd["id"],),
|
|
46
|
+
).fetchall()
|
|
47
|
+
samples = {c: [r[i] for r in rows] for i, c in enumerate(cols)}
|
|
48
|
+
entry = {
|
|
49
|
+
"source_dive_id": sd["id"],
|
|
50
|
+
"source_key": sd["source_key"],
|
|
51
|
+
"start_time_utc": sd["start_time_utc"],
|
|
52
|
+
"start_time_local": sd["start_time_local"],
|
|
53
|
+
"duration_s": sd["duration_s"],
|
|
54
|
+
"max_depth_m": sd["max_depth_m"],
|
|
55
|
+
"mode": sd["mode"],
|
|
56
|
+
"header": json.loads(sd["header_json"]),
|
|
57
|
+
"samples": samples,
|
|
58
|
+
}
|
|
59
|
+
key = sd["source"]
|
|
60
|
+
if key in out["sources"]: # several logs from one source (splits)
|
|
61
|
+
existing = out["sources"][key]
|
|
62
|
+
if isinstance(existing, list):
|
|
63
|
+
existing.append(entry)
|
|
64
|
+
else:
|
|
65
|
+
out["sources"][key] = [existing, entry]
|
|
66
|
+
else:
|
|
67
|
+
out["sources"][key] = entry
|
|
68
|
+
return out
|
|
69
|
+
finally:
|
|
70
|
+
con.close()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def list_dives(db_path: str | Path, include_tests: bool = False) -> list[dict]:
|
|
74
|
+
con = sqlite3.connect(db_path)
|
|
75
|
+
con.row_factory = sqlite3.Row
|
|
76
|
+
try:
|
|
77
|
+
where = "" if include_tests else "WHERE d.is_test = 0"
|
|
78
|
+
return [
|
|
79
|
+
{k: r[k] for k in r.keys()}
|
|
80
|
+
for r in con.execute(
|
|
81
|
+
"SELECT d.*, group_concat(sd.source) AS sources FROM dives d"
|
|
82
|
+
" JOIN dive_members dm ON dm.dive_id = d.id"
|
|
83
|
+
" JOIN source_dives sd ON sd.id = dm.source_dive_id"
|
|
84
|
+
f" {where} GROUP BY d.id ORDER BY d.start_time_utc"
|
|
85
|
+
)
|
|
86
|
+
]
|
|
87
|
+
finally:
|
|
88
|
+
con.close()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""bottomtime command line interface.
|
|
2
|
+
|
|
3
|
+
bottomtime init
|
|
4
|
+
bottomtime ingest garmin <fit-dir>
|
|
5
|
+
bottomtime ingest shearwater <dive_data.db>
|
|
6
|
+
bottomtime match
|
|
7
|
+
bottomtime verify [--xml-dir DIR]
|
|
8
|
+
bottomtime status
|
|
9
|
+
|
|
10
|
+
All commands are idempotent: re-running an ingest skips anything already
|
|
11
|
+
stored (content-hash keyed), match recomputes only 'auto' links.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from .store import db as store_db
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main(argv: list[str] | None = None) -> int:
|
|
24
|
+
parser = argparse.ArgumentParser(prog="bottomtime")
|
|
25
|
+
parser.add_argument("--db", default="data/dives.db", help="path to the dive store")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--archive", default="archive", help="directory for verbatim raw source copies"
|
|
28
|
+
)
|
|
29
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
30
|
+
|
|
31
|
+
sub.add_parser("init", help="create an empty dive store")
|
|
32
|
+
|
|
33
|
+
p_ingest = sub.add_parser("ingest", help="ingest a data source")
|
|
34
|
+
ingest_sub = p_ingest.add_subparsers(dest="source", required=True)
|
|
35
|
+
p_garmin = ingest_sub.add_parser("garmin", help="a directory of FIT files")
|
|
36
|
+
p_garmin.add_argument("fit_dir", type=Path)
|
|
37
|
+
p_garmin.add_argument(
|
|
38
|
+
"--index",
|
|
39
|
+
default="dives_index.json",
|
|
40
|
+
help="Garmin Connect index JSON filename inside fit-dir",
|
|
41
|
+
)
|
|
42
|
+
p_sw = ingest_sub.add_parser("shearwater", help="a Shearwater Cloud dive_data.db")
|
|
43
|
+
p_sw.add_argument("db_path", type=Path)
|
|
44
|
+
|
|
45
|
+
sub.add_parser("match", help="match dual-computer dives, build canonical dives")
|
|
46
|
+
|
|
47
|
+
p_verify = sub.add_parser("verify", help="run the verification suite")
|
|
48
|
+
p_verify.add_argument("--xml-dir", type=Path, default=None,
|
|
49
|
+
help="directory of Shearwater XML exports to cross-check")
|
|
50
|
+
|
|
51
|
+
sub.add_parser("status", help="summarize the store")
|
|
52
|
+
|
|
53
|
+
args = parser.parse_args(argv)
|
|
54
|
+
con = store_db.connect(args.db)
|
|
55
|
+
archive_dir = Path(args.archive)
|
|
56
|
+
|
|
57
|
+
if args.command == "init":
|
|
58
|
+
print(f"initialized {args.db}")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
if args.command == "ingest" and args.source == "garmin":
|
|
62
|
+
from .ingest.garmin_fit import ingest_garmin_dir
|
|
63
|
+
|
|
64
|
+
stats = ingest_garmin_dir(con, archive_dir, args.fit_dir, args.index)
|
|
65
|
+
print(f"garmin: {stats}")
|
|
66
|
+
return 1 if stats["failed"] else 0
|
|
67
|
+
|
|
68
|
+
if args.command == "ingest" and args.source == "shearwater":
|
|
69
|
+
from .ingest.shearwater_db import ingest_shearwater_db
|
|
70
|
+
|
|
71
|
+
stats = ingest_shearwater_db(con, archive_dir, args.db_path)
|
|
72
|
+
print(f"shearwater: {stats}")
|
|
73
|
+
return 1 if stats["failed"] else 0
|
|
74
|
+
|
|
75
|
+
if args.command == "match":
|
|
76
|
+
from .match.matcher import run_match
|
|
77
|
+
|
|
78
|
+
stats = run_match(con)
|
|
79
|
+
print(f"match: {stats}")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
if args.command == "verify":
|
|
83
|
+
from .verify.crosscheck import run_verify
|
|
84
|
+
|
|
85
|
+
return run_verify(con, args.xml_dir)
|
|
86
|
+
|
|
87
|
+
if args.command == "status":
|
|
88
|
+
for line in con.execute(
|
|
89
|
+
"SELECT source || ' source dives: ' || count(*) FROM source_dives GROUP BY source"
|
|
90
|
+
):
|
|
91
|
+
print(line[0])
|
|
92
|
+
for line in con.execute(
|
|
93
|
+
"SELECT 'canonical dives: ' || count(*) || ' (' ||"
|
|
94
|
+
" sum(CASE WHEN is_test THEN 1 ELSE 0 END) || ' test)' FROM dives"
|
|
95
|
+
):
|
|
96
|
+
print(line[0])
|
|
97
|
+
for line in con.execute(
|
|
98
|
+
"SELECT 'matched pairs: ' || count(*) FROM matches WHERE status IN ('auto','confirmed')"
|
|
99
|
+
):
|
|
100
|
+
print(line[0])
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
return 2
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|