bottomtime 0.2.0__tar.gz → 0.3.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.
Files changed (29) hide show
  1. {bottomtime-0.2.0 → bottomtime-0.3.0}/.gitignore +2 -0
  2. {bottomtime-0.2.0 → bottomtime-0.3.0}/PKG-INFO +1 -1
  3. {bottomtime-0.2.0 → bottomtime-0.3.0}/pyproject.toml +1 -1
  4. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/__init__.py +1 -1
  5. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/decode/pnf.py +65 -14
  6. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/shearwater_db.py +6 -3
  7. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/store/schema.py +8 -4
  8. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/verify/crosscheck.py +5 -0
  9. {bottomtime-0.2.0 → bottomtime-0.3.0}/tests/test_pnf.py +42 -2
  10. bottomtime-0.2.0/.claude/settings.local.json +0 -30
  11. {bottomtime-0.2.0 → bottomtime-0.3.0}/.github/workflows/test.yml +0 -0
  12. {bottomtime-0.2.0 → bottomtime-0.3.0}/LICENSE +0 -0
  13. {bottomtime-0.2.0 → bottomtime-0.3.0}/README.md +0 -0
  14. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/api.py +0 -0
  15. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/cli.py +0 -0
  16. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/decode/__init__.py +0 -0
  17. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/__init__.py +0 -0
  18. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/archive.py +0 -0
  19. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/garmin_fit.py +0 -0
  20. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/ingest/shearwater_xml.py +0 -0
  21. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/match/__init__.py +0 -0
  22. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/match/correlate.py +0 -0
  23. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/match/matcher.py +0 -0
  24. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/plot.py +0 -0
  25. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/store/__init__.py +0 -0
  26. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/store/db.py +0 -0
  27. {bottomtime-0.2.0 → bottomtime-0.3.0}/src/bottomtime/verify/__init__.py +0 -0
  28. {bottomtime-0.2.0 → bottomtime-0.3.0}/tests/test_cli.py +0 -0
  29. {bottomtime-0.2.0 → bottomtime-0.3.0}/tests/test_correlate.py +0 -0
@@ -3,6 +3,8 @@ __pycache__/
3
3
  *.egg-info/
4
4
  dist/
5
5
  .pytest_cache/
6
+ .mypy_cache/
7
+ .claude/settings.local.json
6
8
 
7
9
  # Personal dive data lives outside version control
8
10
  data/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bottomtime
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Lossless dive-log store: ingest, decode, and reconcile dives from Garmin (FIT) and Shearwater (Petrel Native Format) dive computers
5
5
  Project-URL: Homepage, https://github.com/snehankekre/bottomtime
6
6
  Project-URL: Issues, https://github.com/snehankekre/bottomtime/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bottomtime"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Lossless dive-log store: ingest, decode, and reconcile dives from Garmin (FIT) and Shearwater (Petrel Native Format) dive computers"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  """bottomtime: lossless dive-log store for multi-computer divers."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  from .api import list_dives, load_dive
6
6
 
@@ -23,6 +23,18 @@ DECODER_VERSION history:
23
23
  dive_details.EndGF99 and decays during surface offgassing; byte 24 sits
24
24
  just below the 3 m-quantized stop depth and zeroes when deco clears;
25
25
  byte 18 equals the XML export's batteryVoltage x 100
26
+ 3 reconciled against Shearwater Cloud's own parser (DiveLogParser.dll,
27
+ class PetrelNativeLogV14Parser, read from the desktop app). Every sample
28
+ byte 0-31 is now named. Changes vs v2:
29
+ - bytes 24/25 are ceiling/gf99 only under gradient-factor deco models;
30
+ under DCIEM (deco model 3) they are a safe-ascent depth instead
31
+ (byte 24 integer metres, byte 25 hundredths), decoded to
32
+ safe_ascent_depth_m with ceiling_m/gf99 left None.
33
+ - battery_v is a 16-bit value: (byte 17 << 8 | byte 18) / 100, not byte 18
34
+ alone (byte 17 is zero only because a ~1.5 V cell never needs it; a
35
+ higher-voltage computer such as a Teric would overflow the old read).
36
+ - byte 11 is battery_pct (BATTERY_PERCENT_REMAINING).
37
+ - bytes 30-31 are sac (surface air consumption, u16 / 100; 0xFFFF = n/a).
26
38
  """
27
39
 
28
40
  from __future__ import annotations
@@ -31,7 +43,7 @@ import gzip
31
43
  import struct
32
44
  from dataclasses import dataclass, field
33
45
 
34
- DECODER_VERSION = 2
46
+ DECODER_VERSION = 3
35
47
 
36
48
  RECORD_SIZE = 32
37
49
 
@@ -44,12 +56,17 @@ INFO_EVENT = 0x30
44
56
  DIVE_SAMPLE_EXT = 0xE1
45
57
  FINAL = 0xFF
46
58
 
47
- # status flag bits (sample byte 12)
59
+ # Sample byte 12 is a packed status/mode byte. Bit meanings per Shearwater's
60
+ # PetrelNativeLogV14Parser: 0 gas-switch-needed, 1 external ppO2, 2 setpoint
61
+ # type (high/low), 3 circuit-switch type, 4 circuit mode (set = OC on this
62
+ # family), 5 CCR mode, 6-7 solenoid-fired count.
48
63
  FLAG_GASSWITCH = 0x01
49
64
  FLAG_PPO2_EXTERNAL = 0x02
50
65
  FLAG_SETPOINT_HIGH = 0x04
51
66
  FLAG_SC = 0x08
52
67
  FLAG_OC = 0x10
68
+ FLAG_CCR_MODE = 0x20
69
+ SOLENOID_COUNT_SHIFT = 6
53
70
 
54
71
  DIVE_MODES = {
55
72
  0: "cc",
@@ -82,10 +99,9 @@ COMPUTER_MODELS = {
82
99
  12: "Tern",
83
100
  }
84
101
 
85
- # Sample bytes not (yet) assigned a meaning; kept per sample as b<offset>.
86
- # 18 (battery), 24 (ceiling), 25 (gf99), 26-27 (@+5) were identified
87
- # empirically and promoted in v2. 30-31 read 0xFFFF (sentinel) so far.
88
- UNMAPPED_SAMPLE_BYTES = (11, 17, 30, 31)
102
+ # As of v3 every sample byte 0-31 is named (reconciled against Shearwater's
103
+ # own PetrelNativeLogV14Parser), so no bytes are stashed raw in extra_json.
104
+ SAC_SENTINEL = 0xFFFF # bytes 30-31 when no air integration / no SAC
89
105
 
90
106
 
91
107
  def _u16be(data: bytes, off: int) -> int:
@@ -127,6 +143,9 @@ class PnfSample:
127
143
  cns_pct: float | None = None
128
144
  gf99: float | None = None
129
145
  ceiling_m: float | None = None
146
+ safe_ascent_depth_m: float | None = None # DCIEM only (bytes 24-25)
147
+ battery_pct: int | None = None
148
+ sac: float | None = None
130
149
  sensor1_raw: int | None = None
131
150
  sensor2_raw: int | None = None
132
151
  sensor3_raw: int | None = None
@@ -135,6 +154,13 @@ class PnfSample:
135
154
  sensor3_ppo2: float | None = None
136
155
  battery_v: float | None = None
137
156
  status_flags: int | None = None
157
+ gas_switch_needed: bool | None = None
158
+ ppo2_external: bool | None = None
159
+ setpoint_high: bool | None = None
160
+ circuit_switch: bool | None = None
161
+ circuit_mode: int | None = None
162
+ ccr_mode: int | None = None
163
+ solenoid_fired_count: int | None = None
138
164
  tank0_psi: float | None = None
139
165
  tank1_psi: float | None = None
140
166
  gas_time_min: float | None = None
@@ -230,11 +256,15 @@ def decode(blob: bytes) -> PnfDive:
230
256
  seq = 0
231
257
  t = 0.0
232
258
 
259
+ is_dciem = header["deco_model"] == "dciem"
260
+
233
261
  for rtype, off in raw_records:
234
262
  rec = data[off : off + RECORD_SIZE]
235
263
  if rtype in (DIVE_SAMPLE, AVELO_SAMPLE):
236
264
  samples.append(
237
- _parse_sample(rec, t, imperial, calibrated, calibration, rtype)
265
+ _parse_sample(
266
+ rec, t, imperial, calibrated, calibration, rtype, is_dciem
267
+ )
238
268
  )
239
269
  t += interval_ms / 1000.0
240
270
  elif rtype == FREEDIVE_SAMPLE:
@@ -295,6 +325,7 @@ def _parse_sample(
295
325
  calibrated: int,
296
326
  calibration: list[float],
297
327
  rtype: int,
328
+ is_dciem: bool,
298
329
  ) -> PnfSample:
299
330
  ft = 0.3048
300
331
  depth = _u16be(rec, 1) / 10.0
@@ -311,6 +342,7 @@ def _parse_sample(
311
342
 
312
343
  status = rec[12] if rtype != AVELO_SAMPLE else 0
313
344
  ccr = (status & FLAG_OC) == 0 if rtype != AVELO_SAMPLE else False
345
+ decoded_status = rtype != AVELO_SAMPLE
314
346
 
315
347
  sample = PnfSample(
316
348
  t_s=t,
@@ -328,11 +360,34 @@ def _parse_sample(
328
360
  sensor2_raw=rec[15],
329
361
  sensor3_raw=rec[16],
330
362
  cns_pct=float(rec[23]),
331
- ceiling_m=float(rec[24]) if rec[24] != 0xFF else None,
332
- gf99=float(rec[25]) if rec[25] != 0xFF else None, # 0xFF = not available
333
- battery_v=rec[18] / 100.0,
363
+ battery_pct=rec[11],
364
+ battery_v=((rec[17] << 8) | rec[18]) / 100.0,
334
365
  )
335
366
 
367
+ if decoded_status:
368
+ sample.gas_switch_needed = bool(status & FLAG_GASSWITCH)
369
+ sample.ppo2_external = bool(status & FLAG_PPO2_EXTERNAL)
370
+ sample.setpoint_high = bool(status & FLAG_SETPOINT_HIGH)
371
+ sample.circuit_switch = bool(status & FLAG_SC)
372
+ sample.circuit_mode = (status >> 4) & 1
373
+ sample.ccr_mode = (status >> 5) & 1
374
+ sample.solenoid_fired_count = (status >> SOLENOID_COUNT_SHIFT) & 3
375
+
376
+ # Bytes 24-25 are ceiling + GF99 under gradient-factor deco models. Under
377
+ # DCIEM (deco model 3) the same two bytes are a safe-ascent depth: byte 24
378
+ # whole metres, byte 25 hundredths, subtracted.
379
+ if is_dciem:
380
+ sample.safe_ascent_depth_m = rec[24] - rec[25] / 100.0
381
+ else:
382
+ if rec[24] != 0xFF:
383
+ sample.ceiling_m = float(rec[24])
384
+ if rec[25] != 0xFF: # 0xFF = not available (still on-gassing)
385
+ sample.gf99 = float(rec[25])
386
+
387
+ sac_raw = _u16be(rec, 30)
388
+ if sac_raw != SAC_SENTINEL:
389
+ sample.sac = sac_raw / 100.0
390
+
336
391
  if ccr:
337
392
  sample.setpoint = rec[19] / 100.0
338
393
  if not (status & FLAG_PPO2_EXTERNAL):
@@ -363,10 +418,6 @@ def _parse_sample(
363
418
  if at_plus_five != 0xFFFF:
364
419
  sample.extra["at_plus_five_min"] = at_plus_five
365
420
 
366
- for b in UNMAPPED_SAMPLE_BYTES:
367
- if rec[b]:
368
- sample.extra[f"b{b}"] = rec[b]
369
-
370
421
  return sample
371
422
 
372
423
 
@@ -139,19 +139,22 @@ def _ingest_log(
139
139
  con.executemany(
140
140
  "INSERT INTO shearwater_samples (source_dive_id, t_s, depth_m, temp_c,"
141
141
  " stop_depth_m, stop_or_ndl_min, tts_min, in_deco, avg_ppo2, o2_pct, he_pct,"
142
- " setpoint, cns_pct, gf99, ceiling_m, sensor1_raw, sensor2_raw, sensor3_raw,"
142
+ " setpoint, cns_pct, gf99, ceiling_m, safe_ascent_depth_m, battery_pct, sac,"
143
+ " sensor1_raw, sensor2_raw, sensor3_raw,"
143
144
  " sensor1_ppo2, sensor2_ppo2, sensor3_ppo2, battery_v, status_flags,"
144
- " tank0_psi, tank1_psi, gas_time_min, extra_json)"
145
- " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
145
+ " solenoid_fired_count, tank0_psi, tank1_psi, gas_time_min, extra_json)"
146
+ " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
146
147
  [
147
148
  (
148
149
  dive_id, s.t_s, s.depth_m, s.temp_c,
149
150
  s.stop_depth_m, s.stop_or_ndl_min, s.tts_min, s.in_deco,
150
151
  s.avg_ppo2, s.o2_pct, s.he_pct,
151
152
  s.setpoint, s.cns_pct, s.gf99, s.ceiling_m,
153
+ s.safe_ascent_depth_m, s.battery_pct, s.sac,
152
154
  s.sensor1_raw, s.sensor2_raw, s.sensor3_raw,
153
155
  s.sensor1_ppo2, s.sensor2_ppo2, s.sensor3_ppo2,
154
156
  s.battery_v, s.status_flags,
157
+ s.solenoid_fired_count,
155
158
  s.tank0_psi, s.tank1_psi, s.gas_time_min,
156
159
  json.dumps(s.extra, separators=(",", ":")) if s.extra else None,
157
160
  )
@@ -11,7 +11,7 @@ Design principles:
11
11
  dives is the canonical entity built from matches + singletons.
12
12
  """
13
13
 
14
- SCHEMA_VERSION = 1
14
+ SCHEMA_VERSION = 2
15
15
 
16
16
  DDL = """
17
17
  CREATE TABLE IF NOT EXISTS meta (
@@ -89,8 +89,11 @@ CREATE TABLE IF NOT EXISTS shearwater_samples (
89
89
  he_pct REAL,
90
90
  setpoint REAL,
91
91
  cns_pct REAL,
92
- gf99 REAL, -- populated at decoder_version >= 2
93
- ceiling_m REAL, -- populated at decoder_version >= 2
92
+ gf99 REAL, -- populated at decoder_version >= 2; NULL under DCIEM
93
+ ceiling_m REAL, -- populated at decoder_version >= 2; NULL under DCIEM
94
+ safe_ascent_depth_m REAL, -- DCIEM only (bytes 24-25); NULL otherwise, v3
95
+ battery_pct INTEGER, -- battery percent remaining (byte 11), v3
96
+ sac REAL, -- surface air consumption (bytes 30-31), v3
94
97
  sensor1_raw INTEGER,
95
98
  sensor2_raw INTEGER,
96
99
  sensor3_raw INTEGER,
@@ -98,7 +101,8 @@ CREATE TABLE IF NOT EXISTS shearwater_samples (
98
101
  sensor2_ppo2 REAL,
99
102
  sensor3_ppo2 REAL,
100
103
  battery_v REAL,
101
- status_flags INTEGER,
104
+ status_flags INTEGER, -- raw sample byte 12 (packed mode/status bits)
105
+ solenoid_fired_count INTEGER, -- CCR O2 solenoid firings, byte 12 bits 6-7, v3
102
106
  tank0_psi REAL,
103
107
  tank1_psi REAL,
104
108
  gas_time_min REAL,
@@ -30,6 +30,7 @@ TOL = {
30
30
  "temp_c": 0.51,
31
31
  "battery_v": 0.011,
32
32
  "ndl_min": 0.001,
33
+ "sac": 0.011,
33
34
  }
34
35
 
35
36
 
@@ -71,6 +72,10 @@ def check_xml(con: sqlite3.Connection, xml_dir: Path, failures: list) -> None:
71
72
  ("averagePPO2", "avg_ppo2", 1.0),
72
73
  ("waterTemp", "temp_c", 1.0),
73
74
  ("batteryVoltage", "battery_v", 1.0),
75
+ # SAC (bytes 30-31) only present with wireless air integration. The
76
+ # XML is produced by the same parser, so it should match exactly;
77
+ # inert on dives without AI (both sides None).
78
+ ("sac", "sac", 1.0),
74
79
  ]
75
80
  for i, (db_row, x) in enumerate(zip(samples, xs)):
76
81
  for xml_field, col, scale in pairs:
@@ -25,8 +25,10 @@ def _sample(depth_dm: int, **kw) -> bytearray:
25
25
  rec[8] = kw.get("o2", 21)
26
26
  rec[9] = kw.get("he", 0)
27
27
  rec[10] = kw.get("ndl_min", 99)
28
+ rec[11] = kw.get("battery_pct", 100)
28
29
  rec[12] = kw.get("flags", pnf.FLAG_OC)
29
30
  rec[14] = kw.get("temp", 27) & 0xFF
31
+ rec[17] = kw.get("battery_msb", 0)
30
32
  rec[18] = kw.get("battery_raw", 152)
31
33
  struct.pack_into(">H", rec, 20, 0xFFFF) # AI off
32
34
  rec[22] = 0xFF # gas time not paired
@@ -35,10 +37,11 @@ def _sample(depth_dm: int, **kw) -> bytearray:
35
37
  rec[25] = kw.get("gf99", 0xFF)
36
38
  struct.pack_into(">H", rec, 26, kw.get("at_plus_five", 0))
37
39
  struct.pack_into(">H", rec, 28, 0xFFFF) # AI off
40
+ struct.pack_into(">H", rec, 30, kw.get("sac_raw", 0xFFFF)) # SAC n/a
38
41
  return rec
39
42
 
40
43
 
41
- def build_blob(samples=None, interval_ms=10000, extra_records=None) -> bytes:
44
+ def build_blob(samples=None, interval_ms=10000, extra_records=None, deco_model=0) -> bytes:
42
45
  records = []
43
46
 
44
47
  opening0 = _record(0x10)
@@ -53,7 +56,7 @@ def build_blob(samples=None, interval_ms=10000, extra_records=None) -> bytes:
53
56
  records.append(opening1)
54
57
 
55
58
  opening2 = _record(0x12)
56
- opening2[18] = 0 # GF deco model
59
+ opening2[18] = deco_model # 0=GF, 1=VPM-B, 2=VPM-B/GFS, 3=DCIEM
57
60
  records.append(opening2)
58
61
 
59
62
  opening3 = _record(0x13)
@@ -123,6 +126,43 @@ def test_samples():
123
126
  assert s1.depth_m == 52.8
124
127
  assert (s1.stop_depth_m, s1.tts_min, s1.in_deco) == (9.0, 14.0, 1)
125
128
  assert (s1.ceiling_m, s1.gf99, s1.cns_pct) == (9.0, 30.0, 4.0)
129
+ assert s0.battery_pct == 100
130
+ assert s0.sac is None # 0xFFFF sentinel
131
+
132
+
133
+ def test_battery_voltage_is_16bit():
134
+ # A higher-voltage computer (e.g. Teric rechargeable ~4.2 V) overflows a
135
+ # single byte, so the decoder must read bytes 17-18 together.
136
+ dive = pnf.decode(build_blob(samples=[_sample(24, battery_msb=1, battery_raw=164)]))
137
+ assert dive.samples[0].battery_v == round((1 << 8 | 164) / 100.0, 2) # 4.20 V
138
+
139
+
140
+ def test_sac_when_present():
141
+ dive = pnf.decode(build_blob(samples=[_sample(24, sac_raw=1234)]))
142
+ assert dive.samples[0].sac == 12.34
143
+
144
+
145
+ def test_status_byte_bits_decoded():
146
+ flags = 0x01 | 0x20 | (2 << 6) # gas-switch-needed + CCR mode + solenoid count 2
147
+ dive = pnf.decode(build_blob(samples=[_sample(24, flags=flags)]))
148
+ s = dive.samples[0]
149
+ assert s.gas_switch_needed is True
150
+ assert s.ccr_mode == 1
151
+ assert s.solenoid_fired_count == 2
152
+ assert s.circuit_mode == 0
153
+ assert s.setpoint_high is False
154
+ assert s.status_flags == flags
155
+
156
+
157
+ def test_dciem_uses_safe_ascent_depth():
158
+ # Under DCIEM (deco model 3), bytes 24-25 are a safe-ascent depth, not
159
+ # ceiling/GF99: byte 24 whole metres minus byte 25 hundredths.
160
+ blob = build_blob(samples=[_sample(300, ceiling=6, gf99=50)], deco_model=3)
161
+ dive = pnf.decode(blob)
162
+ assert dive.header["deco_model"] == "dciem"
163
+ s = dive.samples[0]
164
+ assert s.ceiling_m is None and s.gf99 is None
165
+ assert s.safe_ascent_depth_m == 6 - 0.50 # 5.5 m
126
166
 
127
167
 
128
168
  def test_unknown_records_preserved():
@@ -1,30 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(sqlite3 \"file:data/dives.db?mode=ro\" \"SELECT source, count(*) FROM source_dives GROUP BY source;\" \"SELECT json_extract(header_json,'$.pnf.computer_model'), json_extract(header_json,'$.pnf.computer_firmware'), json_extract(header_json,'$.dive_details.Product'), count(*) FROM source_dives WHERE source='shearwater' GROUP BY 1,2,3;\")",
5
- "Bash(sqlite3 \"file:data/dives.db?mode=ro\" \"SELECT json_extract(header_json,'$.file_id_mesgs[0].product_name'), json_extract(header_json,'$.file_id_mesgs[0].manufacturer'), json_extract(header_json,'$.file_id_mesgs[0].product'), count(*) FROM source_dives WHERE source='garmin' GROUP BY 1,2,3 LIMIT 10;\")",
6
- "Bash(sqlite3 \"file:$(pwd)/data/dives.db?mode=ro&immutable=1\" \"SELECT source, count(*) FROM source_dives GROUP BY source;\")",
7
- "Bash(DB=\"file:$(pwd)/data/dives.db?mode=ro&immutable=1\")",
8
- "Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.pnf.computer_model''), json_extract(header_json,''$.pnf.computer_firmware''), json_extract(header_json,''$.dive_details.Product''), count(*) FROM source_dives WHERE source=''shearwater'' GROUP BY 1,2,3;\")",
9
- "Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.file_id_mesgs[0].product_name''), json_extract(header_json,''$.device_info_mesgs[0].product_name''), json_extract(header_json,''$.device_info_mesgs[0].software_version''), count(*) FROM source_dives WHERE source=''garmin'' GROUP BY 1,2,3 LIMIT 10;\")",
10
- "Bash(DB=\"file:$(pwd)/data/dives.db?mode=ro&immutable=1\")",
11
- "Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.file_id_mesgs[0]'') FROM source_dives WHERE source=''garmin'' LIMIT 1;\")",
12
- "Bash(sqlite3 \"$DB\" \"SELECT json_extract(header_json,''$.device_info_mesgs[0]'') FROM source_dives WHERE source=''garmin'' LIMIT 1;\")",
13
- "Bash(sqlite3 \"$DB\" \"SELECT DISTINCT json_extract(header_json,''$.file_id_mesgs[0].garmin_product'') FROM source_dives WHERE source=''garmin'';\")",
14
- "Bash(TMP=/var/folders/xx/8wmmss0x7lb3v3mrhc1rzzc00000gn/T/tmp.yC0jxrHEY0)",
15
- "Bash(python -c \"import matplotlib; print(matplotlib.__version__)\")",
16
- "Bash(python -m bottomtime.cli --db \"$TMP/dives.db\" plot 318 -o \"$TMP/d318.png\")",
17
- "Bash(python -c \"\nfrom bottomtime.api import load_dive\nfrom bottomtime.plot import aligned_series\nd = load_dive(''$TMP/dives.db'', 318)\nfor s in aligned_series(d):\n print(s[''source''], s[''label''], ''x0=%.0f'' % s[''x''][0], ''xN=%.0f'' % s[''x''][-1])\n\")",
18
- "Bash(python -m pytest tests/ -q 2 >& 1)",
19
- "Bash(python -c \"import sys; print(sys.prefix != sys.base_prefix, sys.prefix)\")",
20
- "Bash(rg -n \"—|–\" --no-heading /Users/snehan/Downloads/bottomtime/README.md /Users/snehan/Downloads/bottomtime/src /Users/snehan/Downloads/bottomtime/tests /Users/snehan/Downloads/bottomtime/pyproject.toml)",
21
- "Bash(rg -ni \"leverage|seamless|robust|delve|deep dive|journey|landscape|ecosystem|empower|unlock|supercharge|elevate|streamline|game-chang|cutting-edge|state-of-the-art|best-in-class|thrilled|excited|passionate|humbled|honored|compelling|actionable|utilize|in order to|testament|underscores|boasts|not just|isn''t about|not about\" --no-heading /Users/snehan/Downloads/bottomtime/README.md /Users/snehan/Downloads/bottomtime/src /Users/snehan/Downloads/bottomtime/tests)",
22
- "Bash(ls -R /Users/snehan/Downloads/snehankekre.github.io/.claude 2 > /dev/null)",
23
- "Bash(ls /Users/snehan/.claude/projects/-Users-snehan-Downloads-snehankekre-github-io/memory/ 2>/dev/null)",
24
- "Bash(rg -n \"—|–\" /Users/snehan/Downloads/bottomtime/README.md /Users/snehan/Downloads/bottomtime/src /Users/snehan/Downloads/bottomtime/tests /Users/snehan/Downloads/bottomtime/pyproject.toml)",
25
- "Bash(echo \"clean: $?\")",
26
- "Bash(python -m pytest tests/ -q)"
27
- ],
28
- "deny": []
29
- }
30
- }
File without changes
File without changes
File without changes