signalk-cli 2.1.0__tar.gz → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalk-cli
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: Query SignalK v2 APIs and export data as CSV, JSON or Apache Arrow Feather
5
5
  Keywords: signalk,sailing,marine,nmea,boating
6
6
  Author: jey burrows
@@ -486,9 +486,10 @@ interchangeable:
486
486
  | `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
487
487
  | `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
488
488
  | `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
489
- | `--format [csv\|json\|raw\|feather]` | from extension, else csv | Output format. `json` is JSON Lines (one row object per line, suitable for a live stream). `raw` is the exact delta message text, one per line. `feather` requires `pip install 'signalk-cli[feather]'` and `--output` (cannot stream to stdout). |
489
+ | `--format [csv\|json\|raw\|values\|feather]` | from extension, else csv | Output format. `json` is JSON Lines (one row object per line, suitable for a live stream). `raw` is the exact delta message text, one per line. `values` is the bare value only, one per line — no other columns. `feather` requires `pip install 'signalk-cli[feather]'` and `--output` (cannot stream to stdout). |
490
490
  | `--no-header` | — | Suppress the CSV header row |
491
491
  | `--include-meta` | — | Also emit rows for `meta` entries (units, description, zones, etc.), not just `values`. Adds a `kind` column (`value`/`meta`) to csv/json/feather output. Ignored for `--format raw`, which always includes meta as-is. |
492
+ | `--source PATTERN` | — | Only include updates whose `$source` matches `PATTERN`. Repeatable, OR'd together. Substring match unless `PATTERN` contains a glob metacharacter (`*`/`?`/`[`), e.g. `--source Teltonika` or `--source '*.GP'`. Client-side, applied after receipt: `csv`/`json`/`values`/`feather` filter per-update; `raw` (whole message, verbatim) passes a message through if *any* of its updates match. |
492
493
  | `-o, --output [FILE]` | stdout | Write to a file. Omit the filename (`--output` alone) to auto-name as `signalk-stream-<server>-<timestamp>.<ext>`. Required for `--format feather`. |
493
494
  | `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
494
495
  | `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
@@ -500,6 +501,8 @@ interchangeable:
500
501
 
501
502
  **raw**: the exact delta message JSON as received from the server, one message per line.
502
503
 
504
+ **values**: just the `value` column, one per line — no timestamp/context/source/path/kind. Best for piping a single path's readings straight into another tool or script. With `--include-meta`, meta values are interleaved in too, indistinguishable from data values (there's no `kind` column to tell them apart) — generally only useful combined with `--source`/PATH filtering down to one thing.
505
+
503
506
  **feather**: Apache Arrow Feather binary format, same columns as csv/json. Requires `pip install 'signalk-cli[feather]'`. Unlike the other formats, rows are buffered in memory across all received messages and written once the session ends (`--count` reached, or Ctrl-C with `--follow`) — cannot be streamed to stdout.
504
507
 
505
508
  #### Examples
@@ -536,6 +539,13 @@ python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feat
536
539
 
537
540
  # Capture until Ctrl-C to a named Feather file
538
541
  python -m signalk_cli.stream deltas --host 10.36.10.21 --follow -o capture.feather 'navigation.*'
542
+
543
+ # Only updates from one sensor (substring match on $source)
544
+ python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --source Teltonika 'navigation.*'
545
+
546
+ # Bare speed values from one sensor, piped straight into another tool
547
+ python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --format values \
548
+ --source Teltonika --bare navigation.speedOverGround
539
549
  ```
540
550
 
541
551
  ---
@@ -458,9 +458,10 @@ interchangeable:
458
458
  | `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
459
459
  | `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
460
460
  | `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
461
- | `--format [csv\|json\|raw\|feather]` | from extension, else csv | Output format. `json` is JSON Lines (one row object per line, suitable for a live stream). `raw` is the exact delta message text, one per line. `feather` requires `pip install 'signalk-cli[feather]'` and `--output` (cannot stream to stdout). |
461
+ | `--format [csv\|json\|raw\|values\|feather]` | from extension, else csv | Output format. `json` is JSON Lines (one row object per line, suitable for a live stream). `raw` is the exact delta message text, one per line. `values` is the bare value only, one per line — no other columns. `feather` requires `pip install 'signalk-cli[feather]'` and `--output` (cannot stream to stdout). |
462
462
  | `--no-header` | — | Suppress the CSV header row |
463
463
  | `--include-meta` | — | Also emit rows for `meta` entries (units, description, zones, etc.), not just `values`. Adds a `kind` column (`value`/`meta`) to csv/json/feather output. Ignored for `--format raw`, which always includes meta as-is. |
464
+ | `--source PATTERN` | — | Only include updates whose `$source` matches `PATTERN`. Repeatable, OR'd together. Substring match unless `PATTERN` contains a glob metacharacter (`*`/`?`/`[`), e.g. `--source Teltonika` or `--source '*.GP'`. Client-side, applied after receipt: `csv`/`json`/`values`/`feather` filter per-update; `raw` (whole message, verbatim) passes a message through if *any* of its updates match. |
464
465
  | `-o, --output [FILE]` | stdout | Write to a file. Omit the filename (`--output` alone) to auto-name as `signalk-stream-<server>-<timestamp>.<ext>`. Required for `--format feather`. |
465
466
  | `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
466
467
  | `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
@@ -472,6 +473,8 @@ interchangeable:
472
473
 
473
474
  **raw**: the exact delta message JSON as received from the server, one message per line.
474
475
 
476
+ **values**: just the `value` column, one per line — no timestamp/context/source/path/kind. Best for piping a single path's readings straight into another tool or script. With `--include-meta`, meta values are interleaved in too, indistinguishable from data values (there's no `kind` column to tell them apart) — generally only useful combined with `--source`/PATH filtering down to one thing.
477
+
475
478
  **feather**: Apache Arrow Feather binary format, same columns as csv/json. Requires `pip install 'signalk-cli[feather]'`. Unlike the other formats, rows are buffered in memory across all received messages and written once the session ends (`--count` reached, or Ctrl-C with `--follow`) — cannot be streamed to stdout.
476
479
 
477
480
  #### Examples
@@ -508,6 +511,13 @@ python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feat
508
511
 
509
512
  # Capture until Ctrl-C to a named Feather file
510
513
  python -m signalk_cli.stream deltas --host 10.36.10.21 --follow -o capture.feather 'navigation.*'
514
+
515
+ # Only updates from one sensor (substring match on $source)
516
+ python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --source Teltonika 'navigation.*'
517
+
518
+ # Bare speed values from one sensor, piped straight into another tool
519
+ python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --format values \
520
+ --source Teltonika --bare navigation.speedOverGround
511
521
  ```
512
522
 
513
523
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "signalk-cli"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  description = "Query SignalK v2 APIs and export data as CSV, JSON or Apache Arrow Feather"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "signalk-cli"
3
- version = "2.1.0"
3
+ version = "2.2.0"
4
4
  description = "Query SignalK v2 APIs and export data as CSV, JSON or Apache Arrow Feather"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -13,11 +13,13 @@ import niquests
13
13
  from ..net import api_error, bare_option, host_option, resolve_host, stderr_ctx
14
14
  from .output import (
15
15
  FEATHER_EXTENSIONS,
16
+ delta_matches_source,
16
17
  extract_delta_rows,
17
18
  write_csv_delta,
18
19
  write_csv_header,
19
20
  write_feather_rows,
20
21
  write_json_delta,
22
+ write_values_delta,
21
23
  )
22
24
  from .stream_api import (
23
25
  SUBSCRIBE_POLICIES,
@@ -103,10 +105,13 @@ def cli():
103
105
  "--format",
104
106
  "fmt",
105
107
  default=None,
106
- type=click.Choice(["csv", "json", "raw", "feather"], case_sensitive=False),
108
+ type=click.Choice(
109
+ ["csv", "json", "raw", "feather", "values"], case_sensitive=False
110
+ ),
107
111
  help="Output format (default: inferred from --output extension, else csv). "
108
112
  "json is JSON Lines (one row object per line). raw is the exact delta "
109
- "message text, one per line. feather requires "
113
+ "message text, one per line. values is the bare value only, one per "
114
+ "line — no timestamp/context/source/path/kind columns. feather requires "
110
115
  "pip install 'signalk-cli[feather]' and --output (cannot stream to stdout).",
111
116
  )
112
117
  @click.option("--no-header", is_flag=True, help="Suppress header row (CSV only)")
@@ -117,6 +122,19 @@ def cli():
117
122
  "not just 'values'. Adds a 'kind' column (value/meta) to csv/json/feather "
118
123
  "output. Ignored for --format raw, which always includes meta as-is.",
119
124
  )
125
+ @click.option(
126
+ "--source",
127
+ "source",
128
+ multiple=True,
129
+ metavar="PATTERN",
130
+ help="Only include updates whose $source matches PATTERN. Repeatable "
131
+ "(OR'd together). PATTERN is a substring match unless it contains a "
132
+ "glob metacharacter (*/?/[), in which case it's matched as a glob, "
133
+ "e.g. --source Teltonika or --source '*.GP'. Filtering is client-side, "
134
+ "applied after receipt — for --format raw (whole message, verbatim) a "
135
+ "message passes if ANY of its updates match; other formats filter "
136
+ "per-update.",
137
+ )
120
138
  @click.option(
121
139
  "--output",
122
140
  "-o",
@@ -156,6 +174,7 @@ def deltas(
156
174
  fmt,
157
175
  no_header,
158
176
  include_meta,
177
+ source,
159
178
  output,
160
179
  follow,
161
180
  count,
@@ -164,7 +183,8 @@ def deltas(
164
183
  """Stream live delta updates from the SignalK v1 Streaming API.
165
184
 
166
185
  Connects via WebSocket and prints delta messages as they arrive, in
167
- csv, json (JSON Lines), raw, or Feather format.
186
+ csv, json (JSON Lines), raw, values (bare value only), or Feather
187
+ format.
168
188
 
169
189
  Always sends an explicit subscribe message for --context, covering
170
190
  PATH arguments if given, otherwise every path ('*'). PATH arguments
@@ -196,6 +216,10 @@ def deltas(
196
216
 
197
217
  # Capture 100 messages to a Feather file (requires signalk-cli[feather])
198
218
  signalk_cli.stream deltas --host 10.36.10.21 --count 100 -o capture.feather
219
+
220
+ # Bare speed values from one sensor, piped straight into another tool
221
+ signalk_cli.stream deltas --host 10.36.10.21 --follow --format values \\
222
+ --source Teltonika --bare navigation.speedOverGround
199
223
  """
200
224
  with stderr_ctx(bare):
201
225
  host = resolve_host(host, no_cache)
@@ -224,6 +248,8 @@ def deltas(
224
248
  if fmt == "feather"
225
249
  else ".json"
226
250
  if fmt in ("json", "raw")
251
+ else ".txt"
252
+ if fmt == "values"
227
253
  else ".csv"
228
254
  )
229
255
  output = f"signalk-stream-{server_name}-{ts}{ext}"
@@ -280,20 +306,29 @@ def deltas(
280
306
  message_count += 1
281
307
  if fmt == "feather":
282
308
  feather_rows.extend(
283
- extract_delta_rows(delta, include_meta=include_meta)
309
+ extract_delta_rows(
310
+ delta, include_meta=include_meta, sources=source
311
+ )
284
312
  )
285
313
  row_total = len(feather_rows)
286
314
  elif fmt == "raw":
287
- click.echo(raw, file=sink)
315
+ if delta_matches_source(delta, source):
316
+ click.echo(raw, file=sink)
288
317
  elif fmt == "json":
289
318
  row_total += write_json_delta(
290
- delta, sink, include_meta=include_meta
319
+ delta, sink, include_meta=include_meta, sources=source
320
+ )
321
+ elif fmt == "values":
322
+ row_total += write_values_delta(
323
+ delta, sink, include_meta=include_meta, sources=source
291
324
  )
292
325
  else:
293
326
  if not header_written and not no_header:
294
327
  write_csv_header(sink, include_meta=include_meta)
295
328
  header_written = True
296
- row_total += write_csv_delta(delta, sink, include_meta=include_meta)
329
+ row_total += write_csv_delta(
330
+ delta, sink, include_meta=include_meta, sources=source
331
+ )
297
332
  except KeyboardInterrupt:
298
333
  pass
299
334
  except niquests.RequestException as e:
@@ -1,6 +1,7 @@
1
1
  """Row extraction and CSV/JSON/Feather writers for SignalK delta messages."""
2
2
 
3
3
  import csv
4
+ import fnmatch
4
5
  import json
5
6
  from typing import IO
6
7
 
@@ -15,8 +16,42 @@ def _normalize_value(value: object) -> str:
15
16
  return str(value)
16
17
 
17
18
 
19
+ def _update_source(update: dict) -> str:
20
+ return update.get("$source") or json.dumps(update.get("source", {}))
21
+
22
+
23
+ def source_matches(source: str, patterns: tuple[str, ...]) -> bool:
24
+ """Match a `$source` string against `--source` filter patterns (OR'd).
25
+
26
+ No patterns means no filtering (always matches). A pattern containing
27
+ glob metacharacters (`*`/`?`/`[`) is matched as-is via `fnmatch`;
28
+ otherwise it's treated as a substring match, e.g. "Teltonika" matches
29
+ the source "Teltonika.GP".
30
+ """
31
+ if not patterns:
32
+ return True
33
+ return any(
34
+ fnmatch.fnmatch(source, p if any(c in p for c in "*?[") else f"*{p}*")
35
+ for p in patterns
36
+ )
37
+
38
+
39
+ def delta_matches_source(delta: dict, patterns: tuple[str, ...]) -> bool:
40
+ """True if any update in the delta has a `$source` matching `patterns`.
41
+
42
+ Used for `--format raw`, which echoes the whole message verbatim and so
43
+ can only filter at message granularity, not per-update.
44
+ """
45
+ if not patterns:
46
+ return True
47
+ return any(
48
+ source_matches(_update_source(update), patterns)
49
+ for update in delta.get("updates", [])
50
+ )
51
+
52
+
18
53
  def extract_delta_rows(
19
- delta: dict, *, include_meta: bool = False
54
+ delta: dict, *, include_meta: bool = False, sources: tuple[str, ...] = ()
20
55
  ) -> list[tuple[str, ...]]:
21
56
  """Flatten a single delta message into rows.
22
57
 
@@ -26,12 +61,18 @@ def extract_delta_rows(
26
61
  update's "meta" entries are included too — per the Streaming API spec,
27
62
  "meta" entries have the same path/value shape but "value" is a metadata
28
63
  object (units, description, zones, etc.), not a telemetry reading.
64
+
65
+ `sources`, if given, drops entire updates whose `$source` doesn't match
66
+ any pattern (see `source_matches`) — filtering is per-update, since
67
+ that's the granularity at which SignalK attaches a source.
29
68
  """
30
69
  context = delta.get("context", "")
31
70
  rows: list[tuple[str, ...]] = []
32
71
  for update in delta.get("updates", []):
72
+ source = _update_source(update)
73
+ if not source_matches(source, sources):
74
+ continue
33
75
  timestamp = update.get("timestamp", "")
34
- source = update.get("$source") or json.dumps(update.get("source", {}))
35
76
  for entry in update.get("values", []):
36
77
  path = entry.get("path", "")
37
78
  value = _normalize_value(entry.get("value"))
@@ -60,9 +101,15 @@ def write_csv_header(sink: IO[str], *, include_meta: bool = False) -> None:
60
101
  sink.flush()
61
102
 
62
103
 
63
- def write_csv_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -> int:
104
+ def write_csv_delta(
105
+ delta: dict,
106
+ sink: IO[str],
107
+ *,
108
+ include_meta: bool = False,
109
+ sources: tuple[str, ...] = (),
110
+ ) -> int:
64
111
  """Write one delta's rows as CSV lines. Returns the number of rows written."""
65
- rows = extract_delta_rows(delta, include_meta=include_meta)
112
+ rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
66
113
  writer = csv.writer(sink)
67
114
  for row in rows:
68
115
  writer.writerow(row)
@@ -70,9 +117,15 @@ def write_csv_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -
70
117
  return len(rows)
71
118
 
72
119
 
73
- def write_json_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -> int:
120
+ def write_json_delta(
121
+ delta: dict,
122
+ sink: IO[str],
123
+ *,
124
+ include_meta: bool = False,
125
+ sources: tuple[str, ...] = (),
126
+ ) -> int:
74
127
  """Write one delta's rows as JSON Lines (one row object per line). Returns row count."""
75
- rows = extract_delta_rows(delta, include_meta=include_meta)
128
+ rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
76
129
  columns = _columns(include_meta)
77
130
  for row in rows:
78
131
  sink.write(json.dumps(dict(zip(columns, row))))
@@ -81,6 +134,26 @@ def write_json_delta(delta: dict, sink: IO[str], *, include_meta: bool = False)
81
134
  return len(rows)
82
135
 
83
136
 
137
+ def write_values_delta(
138
+ delta: dict,
139
+ sink: IO[str],
140
+ *,
141
+ include_meta: bool = False,
142
+ sources: tuple[str, ...] = (),
143
+ ) -> int:
144
+ """Write one delta's bare values, one per line — no other columns.
145
+
146
+ For `--format values`: useful for piping a single path's readings
147
+ straight into another tool/script. Returns the number of values written.
148
+ """
149
+ rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
150
+ for row in rows:
151
+ sink.write(row[-1])
152
+ sink.write("\n")
153
+ sink.flush()
154
+ return len(rows)
155
+
156
+
84
157
  def write_feather_rows(
85
158
  rows: list[tuple[str, ...]], output: str, *, include_meta: bool = False
86
159
  ) -> int: