signalk-cli 2.0.1__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.
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/PKG-INFO +39 -10
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/README.md +38 -9
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/pyproject.toml +6 -3
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/pyproject.toml.orig +4 -3
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/stream/cli.py +74 -17
- signalk_cli-2.2.0/src/signalk_cli/stream/output.py +181 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/stream/stream_api.py +7 -1
- signalk_cli-2.0.1/src/signalk_cli/stream/output.py +0 -89
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/__init__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/__main__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/history/__init__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/history/__main__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/history/cli.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/history/history_api.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/history/output.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/net.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/stream/__init__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.2.0}/src/signalk_cli/stream/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: signalk-cli
|
|
3
|
-
Version: 2.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
|
|
@@ -71,6 +71,10 @@ Use `uv` to run without installing the module permanently, for example:
|
|
|
71
71
|
uv run --with signalk-cli signalk_cli.history list-providers
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
```bash
|
|
75
|
+
uv run --with signalk-cli signalk_cli.stream deltas navigation.position --follow
|
|
76
|
+
```
|
|
77
|
+
|
|
74
78
|
## Running
|
|
75
79
|
|
|
76
80
|
Run via `python -m signalk_cli.history <command>` or `python -m signalk_cli.stream <command>` or without installing the module with `uv run --with signalk-cli signalk_cli.history <command>`.
|
|
@@ -436,8 +440,10 @@ against the server's enumerated `/paths` list:
|
|
|
436
440
|
Quote wildcarded paths (e.g. `'navigation.*'`) so the shell doesn't expand them
|
|
437
441
|
against local filenames first.
|
|
438
442
|
|
|
439
|
-
`deltas` always sends its own explicit subscribe message for `--context
|
|
440
|
-
covering PATH arguments if given, otherwise every
|
|
443
|
+
`deltas` always sends its own explicit subscribe message for `--context`
|
|
444
|
+
(default `vessels.self`), covering PATH arguments if given, otherwise every
|
|
445
|
+
path (`*`). `--context` also accepts the SignalK wildcard: `'*'` or
|
|
446
|
+
`'vessels.*'` subscribes to every vessel, not just your own. `--policy`,
|
|
441
447
|
`--period`, and `--min-period` control that subscription per the
|
|
442
448
|
[Subscription Protocol](https://signalk.org/specification/1.8.2/doc/subscription_protocol.html)'s
|
|
443
449
|
`policy`/`period`/`minPeriod` fields:
|
|
@@ -451,11 +457,23 @@ this CLI doesn't expose it: signalk-server rejects `full` outright ("Only
|
|
|
451
457
|
delta format supported, using it") and always sends delta messages
|
|
452
458
|
regardless, so the choice would be misleading.
|
|
453
459
|
|
|
454
|
-
|
|
455
|
-
parameter (`none`/`self`/`all
|
|
456
|
-
server *additionally* auto-subscribes the connection
|
|
457
|
-
policy/period —
|
|
458
|
-
|
|
460
|
+
**`--subscribe` is a separate, easily-confused mechanism**: it's the
|
|
461
|
+
connection-level `subscribe` query parameter (`none`/`self`/`all`), which
|
|
462
|
+
controls whether the server *additionally* auto-subscribes the connection
|
|
463
|
+
at **its own** default policy/period — you don't get to choose the rate.
|
|
464
|
+
This CLI defaults it to `none`, diverging from the SignalK spec's own
|
|
465
|
+
default of `self`, because `deltas` always sends its own explicit `--context`
|
|
466
|
+
subscription anyway; leaving the connection-level default at `self` would
|
|
467
|
+
just double up on updates for your own vessel.
|
|
468
|
+
|
|
469
|
+
So there are two different ways to see every vessel, and they're not
|
|
470
|
+
interchangeable:
|
|
471
|
+
|
|
472
|
+
| Want | Use | Policy/period used |
|
|
473
|
+
|---|---|---|
|
|
474
|
+
| Just your own vessel | (defaults) | Yours (`--policy`/`--period`) |
|
|
475
|
+
| Every vessel, at your chosen rate | `--context '*'` | Yours (`--policy`/`--period`) |
|
|
476
|
+
| Every vessel, at whatever rate the server defaults to | `--subscribe all` | The server's own default |
|
|
459
477
|
|
|
460
478
|
#### Options
|
|
461
479
|
|
|
@@ -468,8 +486,10 @@ default-policy updates alongside your explicit subscription.
|
|
|
468
486
|
| `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
|
|
469
487
|
| `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
|
|
470
488
|
| `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
|
|
471
|
-
| `--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). |
|
|
472
490
|
| `--no-header` | — | Suppress the CSV header row |
|
|
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. |
|
|
473
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`. |
|
|
474
494
|
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
475
495
|
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
@@ -477,10 +497,12 @@ default-policy updates alongside your explicit subscription.
|
|
|
477
497
|
|
|
478
498
|
#### Output formats
|
|
479
499
|
|
|
480
|
-
**csv** / **json**: `timestamp, context, source, path, value` — one row per `path`/`value` pair in each delta's updates, written incrementally as messages arrive. Structured values (e.g. `navigation.position`) are JSON-encoded in the `value` column.
|
|
500
|
+
**csv** / **json**: `timestamp, context, source, path, value` — one row per `path`/`value` pair in each delta's updates, written incrementally as messages arrive. Structured values (e.g. `navigation.position`) are JSON-encoded in the `value` column. With `--include-meta`, a `kind` column (`value`/`meta`) is inserted before `value`, and `meta` entries (units, description, zones, etc. — also JSON-encoded) are included as rows too; without it, `meta` entries are silently dropped from csv/json/feather (they're still present in `raw`).
|
|
481
501
|
|
|
482
502
|
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
483
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
|
+
|
|
484
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.
|
|
485
507
|
|
|
486
508
|
#### Examples
|
|
@@ -517,6 +539,13 @@ python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feat
|
|
|
517
539
|
|
|
518
540
|
# Capture until Ctrl-C to a named Feather file
|
|
519
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
|
|
520
549
|
```
|
|
521
550
|
|
|
522
551
|
---
|
|
@@ -43,6 +43,10 @@ Use `uv` to run without installing the module permanently, for example:
|
|
|
43
43
|
uv run --with signalk-cli signalk_cli.history list-providers
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
```bash
|
|
47
|
+
uv run --with signalk-cli signalk_cli.stream deltas navigation.position --follow
|
|
48
|
+
```
|
|
49
|
+
|
|
46
50
|
## Running
|
|
47
51
|
|
|
48
52
|
Run via `python -m signalk_cli.history <command>` or `python -m signalk_cli.stream <command>` or without installing the module with `uv run --with signalk-cli signalk_cli.history <command>`.
|
|
@@ -408,8 +412,10 @@ against the server's enumerated `/paths` list:
|
|
|
408
412
|
Quote wildcarded paths (e.g. `'navigation.*'`) so the shell doesn't expand them
|
|
409
413
|
against local filenames first.
|
|
410
414
|
|
|
411
|
-
`deltas` always sends its own explicit subscribe message for `--context
|
|
412
|
-
covering PATH arguments if given, otherwise every
|
|
415
|
+
`deltas` always sends its own explicit subscribe message for `--context`
|
|
416
|
+
(default `vessels.self`), covering PATH arguments if given, otherwise every
|
|
417
|
+
path (`*`). `--context` also accepts the SignalK wildcard: `'*'` or
|
|
418
|
+
`'vessels.*'` subscribes to every vessel, not just your own. `--policy`,
|
|
413
419
|
`--period`, and `--min-period` control that subscription per the
|
|
414
420
|
[Subscription Protocol](https://signalk.org/specification/1.8.2/doc/subscription_protocol.html)'s
|
|
415
421
|
`policy`/`period`/`minPeriod` fields:
|
|
@@ -423,11 +429,23 @@ this CLI doesn't expose it: signalk-server rejects `full` outright ("Only
|
|
|
423
429
|
delta format supported, using it") and always sends delta messages
|
|
424
430
|
regardless, so the choice would be misleading.
|
|
425
431
|
|
|
426
|
-
|
|
427
|
-
parameter (`none`/`self`/`all
|
|
428
|
-
server *additionally* auto-subscribes the connection
|
|
429
|
-
policy/period —
|
|
430
|
-
|
|
432
|
+
**`--subscribe` is a separate, easily-confused mechanism**: it's the
|
|
433
|
+
connection-level `subscribe` query parameter (`none`/`self`/`all`), which
|
|
434
|
+
controls whether the server *additionally* auto-subscribes the connection
|
|
435
|
+
at **its own** default policy/period — you don't get to choose the rate.
|
|
436
|
+
This CLI defaults it to `none`, diverging from the SignalK spec's own
|
|
437
|
+
default of `self`, because `deltas` always sends its own explicit `--context`
|
|
438
|
+
subscription anyway; leaving the connection-level default at `self` would
|
|
439
|
+
just double up on updates for your own vessel.
|
|
440
|
+
|
|
441
|
+
So there are two different ways to see every vessel, and they're not
|
|
442
|
+
interchangeable:
|
|
443
|
+
|
|
444
|
+
| Want | Use | Policy/period used |
|
|
445
|
+
|---|---|---|
|
|
446
|
+
| Just your own vessel | (defaults) | Yours (`--policy`/`--period`) |
|
|
447
|
+
| Every vessel, at your chosen rate | `--context '*'` | Yours (`--policy`/`--period`) |
|
|
448
|
+
| Every vessel, at whatever rate the server defaults to | `--subscribe all` | The server's own default |
|
|
431
449
|
|
|
432
450
|
#### Options
|
|
433
451
|
|
|
@@ -440,8 +458,10 @@ default-policy updates alongside your explicit subscription.
|
|
|
440
458
|
| `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
|
|
441
459
|
| `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
|
|
442
460
|
| `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
|
|
443
|
-
| `--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). |
|
|
444
462
|
| `--no-header` | — | Suppress the CSV header row |
|
|
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. |
|
|
445
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`. |
|
|
446
466
|
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
447
467
|
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
@@ -449,10 +469,12 @@ default-policy updates alongside your explicit subscription.
|
|
|
449
469
|
|
|
450
470
|
#### Output formats
|
|
451
471
|
|
|
452
|
-
**csv** / **json**: `timestamp, context, source, path, value` — one row per `path`/`value` pair in each delta's updates, written incrementally as messages arrive. Structured values (e.g. `navigation.position`) are JSON-encoded in the `value` column.
|
|
472
|
+
**csv** / **json**: `timestamp, context, source, path, value` — one row per `path`/`value` pair in each delta's updates, written incrementally as messages arrive. Structured values (e.g. `navigation.position`) are JSON-encoded in the `value` column. With `--include-meta`, a `kind` column (`value`/`meta`) is inserted before `value`, and `meta` entries (units, description, zones, etc. — also JSON-encoded) are included as rows too; without it, `meta` entries are silently dropped from csv/json/feather (they're still present in `raw`).
|
|
453
473
|
|
|
454
474
|
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
455
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
|
+
|
|
456
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.
|
|
457
479
|
|
|
458
480
|
#### Examples
|
|
@@ -489,6 +511,13 @@ python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feat
|
|
|
489
511
|
|
|
490
512
|
# Capture until Ctrl-C to a named Feather file
|
|
491
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
|
|
492
521
|
```
|
|
493
522
|
|
|
494
523
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "signalk-cli"
|
|
3
|
-
version = "2.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"
|
|
@@ -102,13 +102,16 @@ mypy_path = ["./src"]
|
|
|
102
102
|
[tool.uv]
|
|
103
103
|
compile-bytecode = true
|
|
104
104
|
managed = true
|
|
105
|
-
exclude-newer = "
|
|
105
|
+
exclude-newer = "7 days"
|
|
106
106
|
add-bounds = "major"
|
|
107
107
|
|
|
108
|
+
[tool.uv.exclude-newer-package]
|
|
109
|
+
uv_build = "2 days"
|
|
110
|
+
|
|
108
111
|
[tool.uv.build-backend]
|
|
109
112
|
module-root = "src"
|
|
110
113
|
module-name = "signalk_cli"
|
|
111
114
|
|
|
112
115
|
[build-system]
|
|
113
|
-
requires = ["uv_build>=0.
|
|
116
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
114
117
|
build-backend = "uv_build"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "signalk-cli"
|
|
3
|
-
version = "2.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"
|
|
@@ -101,11 +101,12 @@ mypy_path=["./src"]
|
|
|
101
101
|
[tool.uv]
|
|
102
102
|
compile-bytecode = true
|
|
103
103
|
managed = true
|
|
104
|
-
exclude-newer = "
|
|
104
|
+
exclude-newer = "7 days"
|
|
105
|
+
exclude-newer-package = {"uv_build"="2 days"}
|
|
105
106
|
add-bounds = "major"
|
|
106
107
|
|
|
107
108
|
[build-system]
|
|
108
|
-
requires = ["uv_build>=0.
|
|
109
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
109
110
|
build-backend = "uv_build"
|
|
110
111
|
|
|
111
112
|
|
|
@@ -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,
|
|
@@ -49,18 +51,26 @@ def cli():
|
|
|
49
51
|
@host_option
|
|
50
52
|
@click.option("--no-cache", is_flag=True, help="Ignore cached host")
|
|
51
53
|
@click.option(
|
|
52
|
-
"--context",
|
|
54
|
+
"--context",
|
|
55
|
+
"-c",
|
|
56
|
+
default="vessels.self",
|
|
57
|
+
show_default=True,
|
|
58
|
+
help="SignalK context for the explicit subscription this command "
|
|
59
|
+
"always sends. Also accepts the SignalK wildcard '*' (or 'vessels.*') "
|
|
60
|
+
"to subscribe to every vessel at your own --policy/--period — the "
|
|
61
|
+
"alternative to --subscribe all, which uses the server's default rate.",
|
|
53
62
|
)
|
|
54
63
|
@click.option(
|
|
55
64
|
"--subscribe",
|
|
56
65
|
type=click.Choice(SUBSCRIBE_POLICIES, case_sensitive=False),
|
|
57
66
|
default="none",
|
|
58
67
|
show_default=True,
|
|
59
|
-
help="Connection-level subscribe
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
68
|
+
help="Connection-level auto-subscribe at the server's OWN default "
|
|
69
|
+
"policy/period — separate from, and in addition to, this command's "
|
|
70
|
+
"explicit --context subscription above. Defaults to 'none' (the "
|
|
71
|
+
"SignalK spec's own default is 'self') to avoid double-subscribing "
|
|
72
|
+
"your own context. 'all' adds other vessels at the server's rate; for "
|
|
73
|
+
"other vessels at your chosen rate, use --context '*' instead.",
|
|
64
74
|
)
|
|
65
75
|
@click.option(
|
|
66
76
|
"--policy",
|
|
@@ -95,13 +105,36 @@ def cli():
|
|
|
95
105
|
"--format",
|
|
96
106
|
"fmt",
|
|
97
107
|
default=None,
|
|
98
|
-
type=click.Choice(
|
|
108
|
+
type=click.Choice(
|
|
109
|
+
["csv", "json", "raw", "feather", "values"], case_sensitive=False
|
|
110
|
+
),
|
|
99
111
|
help="Output format (default: inferred from --output extension, else csv). "
|
|
100
112
|
"json is JSON Lines (one row object per line). raw is the exact delta "
|
|
101
|
-
"message text, one per line.
|
|
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 "
|
|
102
115
|
"pip install 'signalk-cli[feather]' and --output (cannot stream to stdout).",
|
|
103
116
|
)
|
|
104
117
|
@click.option("--no-header", is_flag=True, help="Suppress header row (CSV only)")
|
|
118
|
+
@click.option(
|
|
119
|
+
"--include-meta",
|
|
120
|
+
is_flag=True,
|
|
121
|
+
help="Also emit rows for 'meta' entries (units, description, zones, etc.), "
|
|
122
|
+
"not just 'values'. Adds a 'kind' column (value/meta) to csv/json/feather "
|
|
123
|
+
"output. Ignored for --format raw, which always includes meta as-is.",
|
|
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
|
+
)
|
|
105
138
|
@click.option(
|
|
106
139
|
"--output",
|
|
107
140
|
"-o",
|
|
@@ -140,6 +173,8 @@ def deltas(
|
|
|
140
173
|
min_period,
|
|
141
174
|
fmt,
|
|
142
175
|
no_header,
|
|
176
|
+
include_meta,
|
|
177
|
+
source,
|
|
143
178
|
output,
|
|
144
179
|
follow,
|
|
145
180
|
count,
|
|
@@ -148,7 +183,8 @@ def deltas(
|
|
|
148
183
|
"""Stream live delta updates from the SignalK v1 Streaming API.
|
|
149
184
|
|
|
150
185
|
Connects via WebSocket and prints delta messages as they arrive, in
|
|
151
|
-
csv, json (JSON Lines), raw, or Feather
|
|
186
|
+
csv, json (JSON Lines), raw, values (bare value only), or Feather
|
|
187
|
+
format.
|
|
152
188
|
|
|
153
189
|
Always sends an explicit subscribe message for --context, covering
|
|
154
190
|
PATH arguments if given, otherwise every path ('*'). PATH arguments
|
|
@@ -180,6 +216,10 @@ def deltas(
|
|
|
180
216
|
|
|
181
217
|
# Capture 100 messages to a Feather file (requires signalk-cli[feather])
|
|
182
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
|
|
183
223
|
"""
|
|
184
224
|
with stderr_ctx(bare):
|
|
185
225
|
host = resolve_host(host, no_cache)
|
|
@@ -208,6 +248,8 @@ def deltas(
|
|
|
208
248
|
if fmt == "feather"
|
|
209
249
|
else ".json"
|
|
210
250
|
if fmt in ("json", "raw")
|
|
251
|
+
else ".txt"
|
|
252
|
+
if fmt == "values"
|
|
211
253
|
else ".csv"
|
|
212
254
|
)
|
|
213
255
|
output = f"signalk-stream-{server_name}-{ts}{ext}"
|
|
@@ -233,7 +275,7 @@ def deltas(
|
|
|
233
275
|
click.echo(f"Format: {fmt}", err=True)
|
|
234
276
|
|
|
235
277
|
try:
|
|
236
|
-
ws = open_stream(host, subscribe)
|
|
278
|
+
ws = open_stream(host, subscribe, timeout=None if follow else 30)
|
|
237
279
|
except niquests.RequestException as e:
|
|
238
280
|
click.echo(f"Error connecting to stream: {api_error(e)}", err=True)
|
|
239
281
|
sys.exit(1)
|
|
@@ -252,7 +294,7 @@ def deltas(
|
|
|
252
294
|
message_count = 0
|
|
253
295
|
row_total = 0
|
|
254
296
|
header_written = False
|
|
255
|
-
feather_rows: list[tuple[str,
|
|
297
|
+
feather_rows: list[tuple[str, ...]] = []
|
|
256
298
|
fh = (
|
|
257
299
|
open(output, "w", newline="") # noqa: SIM115
|
|
258
300
|
if write_to_file and fmt != "feather"
|
|
@@ -263,26 +305,41 @@ def deltas(
|
|
|
263
305
|
for raw, delta in iter_deltas(ws, effective_count):
|
|
264
306
|
message_count += 1
|
|
265
307
|
if fmt == "feather":
|
|
266
|
-
feather_rows.extend(
|
|
308
|
+
feather_rows.extend(
|
|
309
|
+
extract_delta_rows(
|
|
310
|
+
delta, include_meta=include_meta, sources=source
|
|
311
|
+
)
|
|
312
|
+
)
|
|
267
313
|
row_total = len(feather_rows)
|
|
268
314
|
elif fmt == "raw":
|
|
269
|
-
|
|
315
|
+
if delta_matches_source(delta, source):
|
|
316
|
+
click.echo(raw, file=sink)
|
|
270
317
|
elif fmt == "json":
|
|
271
|
-
row_total += write_json_delta(
|
|
318
|
+
row_total += write_json_delta(
|
|
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
|
|
324
|
+
)
|
|
272
325
|
else:
|
|
273
326
|
if not header_written and not no_header:
|
|
274
|
-
write_csv_header(sink)
|
|
327
|
+
write_csv_header(sink, include_meta=include_meta)
|
|
275
328
|
header_written = True
|
|
276
|
-
row_total += write_csv_delta(
|
|
329
|
+
row_total += write_csv_delta(
|
|
330
|
+
delta, sink, include_meta=include_meta, sources=source
|
|
331
|
+
)
|
|
277
332
|
except KeyboardInterrupt:
|
|
278
333
|
pass
|
|
334
|
+
except niquests.RequestException as e:
|
|
335
|
+
click.echo(f"Stream connection lost: {api_error(e)}", err=True)
|
|
279
336
|
finally:
|
|
280
337
|
ws.close()
|
|
281
338
|
if fh:
|
|
282
339
|
fh.close()
|
|
283
340
|
|
|
284
341
|
if fmt == "feather":
|
|
285
|
-
write_feather_rows(feather_rows, output)
|
|
342
|
+
write_feather_rows(feather_rows, output, include_meta=include_meta)
|
|
286
343
|
|
|
287
344
|
if write_to_file:
|
|
288
345
|
click.echo(f"Wrote {output}", err=True)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Row extraction and CSV/JSON/Feather writers for SignalK delta messages."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import fnmatch
|
|
5
|
+
import json
|
|
6
|
+
from typing import IO
|
|
7
|
+
|
|
8
|
+
FEATHER_EXTENSIONS = {".feather", ".arrow", ".fea"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _normalize_value(value: object) -> str:
|
|
12
|
+
if isinstance(value, (dict, list)):
|
|
13
|
+
return json.dumps(value)
|
|
14
|
+
if value is None:
|
|
15
|
+
return ""
|
|
16
|
+
return str(value)
|
|
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
|
+
|
|
53
|
+
def extract_delta_rows(
|
|
54
|
+
delta: dict, *, include_meta: bool = False, sources: tuple[str, ...] = ()
|
|
55
|
+
) -> list[tuple[str, ...]]:
|
|
56
|
+
"""Flatten a single delta message into rows.
|
|
57
|
+
|
|
58
|
+
Without `include_meta`, rows are (timestamp, context, source, path,
|
|
59
|
+
value) from each update's "values" entries. With `include_meta`, a
|
|
60
|
+
"kind" column ("value"/"meta") is inserted before "value", and each
|
|
61
|
+
update's "meta" entries are included too — per the Streaming API spec,
|
|
62
|
+
"meta" entries have the same path/value shape but "value" is a metadata
|
|
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.
|
|
68
|
+
"""
|
|
69
|
+
context = delta.get("context", "")
|
|
70
|
+
rows: list[tuple[str, ...]] = []
|
|
71
|
+
for update in delta.get("updates", []):
|
|
72
|
+
source = _update_source(update)
|
|
73
|
+
if not source_matches(source, sources):
|
|
74
|
+
continue
|
|
75
|
+
timestamp = update.get("timestamp", "")
|
|
76
|
+
for entry in update.get("values", []):
|
|
77
|
+
path = entry.get("path", "")
|
|
78
|
+
value = _normalize_value(entry.get("value"))
|
|
79
|
+
if include_meta:
|
|
80
|
+
rows.append((timestamp, context, source, path, "value", value))
|
|
81
|
+
else:
|
|
82
|
+
rows.append((timestamp, context, source, path, value))
|
|
83
|
+
if include_meta:
|
|
84
|
+
for entry in update.get("meta", []):
|
|
85
|
+
path = entry.get("path", "")
|
|
86
|
+
value = _normalize_value(entry.get("value"))
|
|
87
|
+
rows.append((timestamp, context, source, path, "meta", value))
|
|
88
|
+
return rows
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
CSV_COLUMNS = ["timestamp", "context", "source", "path", "value"]
|
|
92
|
+
CSV_COLUMNS_WITH_KIND = ["timestamp", "context", "source", "path", "kind", "value"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _columns(include_meta: bool) -> list[str]:
|
|
96
|
+
return CSV_COLUMNS_WITH_KIND if include_meta else CSV_COLUMNS
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def write_csv_header(sink: IO[str], *, include_meta: bool = False) -> None:
|
|
100
|
+
csv.writer(sink).writerow(_columns(include_meta))
|
|
101
|
+
sink.flush()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def write_csv_delta(
|
|
105
|
+
delta: dict,
|
|
106
|
+
sink: IO[str],
|
|
107
|
+
*,
|
|
108
|
+
include_meta: bool = False,
|
|
109
|
+
sources: tuple[str, ...] = (),
|
|
110
|
+
) -> int:
|
|
111
|
+
"""Write one delta's rows as CSV lines. Returns the number of rows written."""
|
|
112
|
+
rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
|
|
113
|
+
writer = csv.writer(sink)
|
|
114
|
+
for row in rows:
|
|
115
|
+
writer.writerow(row)
|
|
116
|
+
sink.flush()
|
|
117
|
+
return len(rows)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def write_json_delta(
|
|
121
|
+
delta: dict,
|
|
122
|
+
sink: IO[str],
|
|
123
|
+
*,
|
|
124
|
+
include_meta: bool = False,
|
|
125
|
+
sources: tuple[str, ...] = (),
|
|
126
|
+
) -> int:
|
|
127
|
+
"""Write one delta's rows as JSON Lines (one row object per line). Returns row count."""
|
|
128
|
+
rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
|
|
129
|
+
columns = _columns(include_meta)
|
|
130
|
+
for row in rows:
|
|
131
|
+
sink.write(json.dumps(dict(zip(columns, row))))
|
|
132
|
+
sink.write("\n")
|
|
133
|
+
sink.flush()
|
|
134
|
+
return len(rows)
|
|
135
|
+
|
|
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
|
+
|
|
157
|
+
def write_feather_rows(
|
|
158
|
+
rows: list[tuple[str, ...]], output: str, *, include_meta: bool = False
|
|
159
|
+
) -> int:
|
|
160
|
+
"""Write accumulated delta rows as Feather. Returns the number of rows written.
|
|
161
|
+
|
|
162
|
+
Unlike CSV/JSON, Feather cannot be appended to incrementally — callers must
|
|
163
|
+
buffer rows across messages and call this once at the end of the session.
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
import pyarrow as pa
|
|
167
|
+
from pyarrow import feather
|
|
168
|
+
except ImportError:
|
|
169
|
+
raise ImportError(
|
|
170
|
+
"pyarrow is required for Feather output: pip install 'signalk-cli[feather]'"
|
|
171
|
+
) from None
|
|
172
|
+
columns = _columns(include_meta)
|
|
173
|
+
row_columns = list(zip(*rows)) if rows else [()] * len(columns)
|
|
174
|
+
table = pa.table(
|
|
175
|
+
{
|
|
176
|
+
name: pa.array(values, type=pa.string())
|
|
177
|
+
for name, values in zip(columns, row_columns)
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
feather.write_feather(table, output)
|
|
181
|
+
return len(rows)
|
|
@@ -24,9 +24,15 @@ def to_ws_url(host: str) -> str:
|
|
|
24
24
|
return urlunparse((scheme, parsed.netloc, STREAM_PATH, "", "", ""))
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def open_stream(host: str, subscribe: str, timeout: float = 30):
|
|
27
|
+
def open_stream(host: str, subscribe: str, timeout: float | None = 30):
|
|
28
28
|
"""Open a WebSocket connection to the SignalK streaming endpoint.
|
|
29
29
|
|
|
30
|
+
`timeout` sets the socket's read timeout for the life of the connection —
|
|
31
|
+
every subsequent `next_payload()` read reuses it, not just the initial
|
|
32
|
+
handshake. Pass `None` when the caller intends to block indefinitely
|
|
33
|
+
between messages (e.g. `--follow`), since deltas can legitimately go
|
|
34
|
+
quiet for longer than any fixed timeout depending on subscribe policy.
|
|
35
|
+
|
|
30
36
|
Returns the underlying HTTP extension object, used to send/receive frames
|
|
31
37
|
via `send_payload`/`next_payload`.
|
|
32
38
|
"""
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"""Row extraction and CSV/JSON/Feather writers for SignalK delta messages."""
|
|
2
|
-
|
|
3
|
-
import csv
|
|
4
|
-
import json
|
|
5
|
-
from typing import IO
|
|
6
|
-
|
|
7
|
-
FEATHER_EXTENSIONS = {".feather", ".arrow", ".fea"}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def extract_delta_rows(delta: dict) -> list[tuple[str, str, str, str, str]]:
|
|
11
|
-
"""Flatten a single delta message into (timestamp, context, source, path, value) rows."""
|
|
12
|
-
context = delta.get("context", "")
|
|
13
|
-
rows = []
|
|
14
|
-
for update in delta.get("updates", []):
|
|
15
|
-
timestamp = update.get("timestamp", "")
|
|
16
|
-
source = update.get("$source") or json.dumps(update.get("source", {}))
|
|
17
|
-
for entry in update.get("values", []):
|
|
18
|
-
path = entry.get("path", "")
|
|
19
|
-
value = entry.get("value")
|
|
20
|
-
if isinstance(value, (dict, list)):
|
|
21
|
-
value = json.dumps(value)
|
|
22
|
-
elif value is None:
|
|
23
|
-
value = ""
|
|
24
|
-
else:
|
|
25
|
-
value = str(value)
|
|
26
|
-
rows.append((timestamp, context, source, path, value))
|
|
27
|
-
return rows
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
CSV_COLUMNS = ["timestamp", "context", "source", "path", "value"]
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def write_csv_header(sink: IO[str]) -> None:
|
|
34
|
-
csv.writer(sink).writerow(CSV_COLUMNS)
|
|
35
|
-
sink.flush()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def write_csv_delta(delta: dict, sink: IO[str]) -> int:
|
|
39
|
-
"""Write one delta's rows as CSV lines. Returns the number of rows written."""
|
|
40
|
-
rows = extract_delta_rows(delta)
|
|
41
|
-
writer = csv.writer(sink)
|
|
42
|
-
for row in rows:
|
|
43
|
-
writer.writerow(row)
|
|
44
|
-
sink.flush()
|
|
45
|
-
return len(rows)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def write_json_delta(delta: dict, sink: IO[str]) -> int:
|
|
49
|
-
"""Write one delta's rows as JSON Lines (one row object per line). Returns row count."""
|
|
50
|
-
rows = extract_delta_rows(delta)
|
|
51
|
-
for ts, context, source, path, value in rows:
|
|
52
|
-
sink.write(
|
|
53
|
-
json.dumps(
|
|
54
|
-
{
|
|
55
|
-
"timestamp": ts,
|
|
56
|
-
"context": context,
|
|
57
|
-
"source": source,
|
|
58
|
-
"path": path,
|
|
59
|
-
"value": value,
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
sink.write("\n")
|
|
64
|
-
sink.flush()
|
|
65
|
-
return len(rows)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def write_feather_rows(rows: list[tuple[str, str, str, str, str]], output: str) -> int:
|
|
69
|
-
"""Write accumulated delta rows as Feather. Returns the number of rows written.
|
|
70
|
-
|
|
71
|
-
Unlike CSV/JSON, Feather cannot be appended to incrementally — callers must
|
|
72
|
-
buffer rows across messages and call this once at the end of the session.
|
|
73
|
-
"""
|
|
74
|
-
try:
|
|
75
|
-
import pyarrow as pa
|
|
76
|
-
from pyarrow import feather
|
|
77
|
-
except ImportError:
|
|
78
|
-
raise ImportError(
|
|
79
|
-
"pyarrow is required for Feather output: pip install 'signalk-cli[feather]'"
|
|
80
|
-
) from None
|
|
81
|
-
columns = list(zip(*rows)) if rows else [()] * len(CSV_COLUMNS)
|
|
82
|
-
table = pa.table(
|
|
83
|
-
{
|
|
84
|
-
name: pa.array(values, type=pa.string())
|
|
85
|
-
for name, values in zip(CSV_COLUMNS, columns)
|
|
86
|
-
}
|
|
87
|
-
)
|
|
88
|
-
feather.write_feather(table, output)
|
|
89
|
-
return len(rows)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|