signalk-cli 2.0.1__tar.gz → 2.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.
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/PKG-INFO +28 -9
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/README.md +27 -8
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/pyproject.toml +6 -3
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/pyproject.toml.orig +4 -3
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/stream/cli.py +35 -13
- signalk_cli-2.1.0/src/signalk_cli/stream/output.py +108 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.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.1.0}/src/signalk_cli/__init__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/__main__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/history/__init__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/history/__main__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/history/cli.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/history/history_api.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/history/output.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/net.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.0}/src/signalk_cli/stream/__init__.py +0 -0
- {signalk_cli-2.0.1 → signalk_cli-2.1.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.1.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
|
|
|
@@ -470,6 +488,7 @@ default-policy updates alongside your explicit subscription.
|
|
|
470
488
|
| `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
|
|
471
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). |
|
|
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. |
|
|
473
492
|
| `-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
493
|
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
475
494
|
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
@@ -477,7 +496,7 @@ default-policy updates alongside your explicit subscription.
|
|
|
477
496
|
|
|
478
497
|
#### Output formats
|
|
479
498
|
|
|
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.
|
|
499
|
+
**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
500
|
|
|
482
501
|
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
483
502
|
|
|
@@ -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
|
|
|
@@ -442,6 +460,7 @@ default-policy updates alongside your explicit subscription.
|
|
|
442
460
|
| `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
|
|
443
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). |
|
|
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. |
|
|
445
464
|
| `-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
465
|
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
447
466
|
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
@@ -449,7 +468,7 @@ default-policy updates alongside your explicit subscription.
|
|
|
449
468
|
|
|
450
469
|
#### Output formats
|
|
451
470
|
|
|
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.
|
|
471
|
+
**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
472
|
|
|
454
473
|
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
455
474
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "signalk-cli"
|
|
3
|
-
version = "2.0
|
|
3
|
+
version = "2.1.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.1.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
|
|
|
@@ -49,18 +49,26 @@ def cli():
|
|
|
49
49
|
@host_option
|
|
50
50
|
@click.option("--no-cache", is_flag=True, help="Ignore cached host")
|
|
51
51
|
@click.option(
|
|
52
|
-
"--context",
|
|
52
|
+
"--context",
|
|
53
|
+
"-c",
|
|
54
|
+
default="vessels.self",
|
|
55
|
+
show_default=True,
|
|
56
|
+
help="SignalK context for the explicit subscription this command "
|
|
57
|
+
"always sends. Also accepts the SignalK wildcard '*' (or 'vessels.*') "
|
|
58
|
+
"to subscribe to every vessel at your own --policy/--period — the "
|
|
59
|
+
"alternative to --subscribe all, which uses the server's default rate.",
|
|
53
60
|
)
|
|
54
61
|
@click.option(
|
|
55
62
|
"--subscribe",
|
|
56
63
|
type=click.Choice(SUBSCRIBE_POLICIES, case_sensitive=False),
|
|
57
64
|
default="none",
|
|
58
65
|
show_default=True,
|
|
59
|
-
help="Connection-level subscribe
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
66
|
+
help="Connection-level auto-subscribe at the server's OWN default "
|
|
67
|
+
"policy/period — separate from, and in addition to, this command's "
|
|
68
|
+
"explicit --context subscription above. Defaults to 'none' (the "
|
|
69
|
+
"SignalK spec's own default is 'self') to avoid double-subscribing "
|
|
70
|
+
"your own context. 'all' adds other vessels at the server's rate; for "
|
|
71
|
+
"other vessels at your chosen rate, use --context '*' instead.",
|
|
64
72
|
)
|
|
65
73
|
@click.option(
|
|
66
74
|
"--policy",
|
|
@@ -102,6 +110,13 @@ def cli():
|
|
|
102
110
|
"pip install 'signalk-cli[feather]' and --output (cannot stream to stdout).",
|
|
103
111
|
)
|
|
104
112
|
@click.option("--no-header", is_flag=True, help="Suppress header row (CSV only)")
|
|
113
|
+
@click.option(
|
|
114
|
+
"--include-meta",
|
|
115
|
+
is_flag=True,
|
|
116
|
+
help="Also emit rows for 'meta' entries (units, description, zones, etc.), "
|
|
117
|
+
"not just 'values'. Adds a 'kind' column (value/meta) to csv/json/feather "
|
|
118
|
+
"output. Ignored for --format raw, which always includes meta as-is.",
|
|
119
|
+
)
|
|
105
120
|
@click.option(
|
|
106
121
|
"--output",
|
|
107
122
|
"-o",
|
|
@@ -140,6 +155,7 @@ def deltas(
|
|
|
140
155
|
min_period,
|
|
141
156
|
fmt,
|
|
142
157
|
no_header,
|
|
158
|
+
include_meta,
|
|
143
159
|
output,
|
|
144
160
|
follow,
|
|
145
161
|
count,
|
|
@@ -233,7 +249,7 @@ def deltas(
|
|
|
233
249
|
click.echo(f"Format: {fmt}", err=True)
|
|
234
250
|
|
|
235
251
|
try:
|
|
236
|
-
ws = open_stream(host, subscribe)
|
|
252
|
+
ws = open_stream(host, subscribe, timeout=None if follow else 30)
|
|
237
253
|
except niquests.RequestException as e:
|
|
238
254
|
click.echo(f"Error connecting to stream: {api_error(e)}", err=True)
|
|
239
255
|
sys.exit(1)
|
|
@@ -252,7 +268,7 @@ def deltas(
|
|
|
252
268
|
message_count = 0
|
|
253
269
|
row_total = 0
|
|
254
270
|
header_written = False
|
|
255
|
-
feather_rows: list[tuple[str,
|
|
271
|
+
feather_rows: list[tuple[str, ...]] = []
|
|
256
272
|
fh = (
|
|
257
273
|
open(output, "w", newline="") # noqa: SIM115
|
|
258
274
|
if write_to_file and fmt != "feather"
|
|
@@ -263,26 +279,32 @@ def deltas(
|
|
|
263
279
|
for raw, delta in iter_deltas(ws, effective_count):
|
|
264
280
|
message_count += 1
|
|
265
281
|
if fmt == "feather":
|
|
266
|
-
feather_rows.extend(
|
|
282
|
+
feather_rows.extend(
|
|
283
|
+
extract_delta_rows(delta, include_meta=include_meta)
|
|
284
|
+
)
|
|
267
285
|
row_total = len(feather_rows)
|
|
268
286
|
elif fmt == "raw":
|
|
269
287
|
click.echo(raw, file=sink)
|
|
270
288
|
elif fmt == "json":
|
|
271
|
-
row_total += write_json_delta(
|
|
289
|
+
row_total += write_json_delta(
|
|
290
|
+
delta, sink, include_meta=include_meta
|
|
291
|
+
)
|
|
272
292
|
else:
|
|
273
293
|
if not header_written and not no_header:
|
|
274
|
-
write_csv_header(sink)
|
|
294
|
+
write_csv_header(sink, include_meta=include_meta)
|
|
275
295
|
header_written = True
|
|
276
|
-
row_total += write_csv_delta(delta, sink)
|
|
296
|
+
row_total += write_csv_delta(delta, sink, include_meta=include_meta)
|
|
277
297
|
except KeyboardInterrupt:
|
|
278
298
|
pass
|
|
299
|
+
except niquests.RequestException as e:
|
|
300
|
+
click.echo(f"Stream connection lost: {api_error(e)}", err=True)
|
|
279
301
|
finally:
|
|
280
302
|
ws.close()
|
|
281
303
|
if fh:
|
|
282
304
|
fh.close()
|
|
283
305
|
|
|
284
306
|
if fmt == "feather":
|
|
285
|
-
write_feather_rows(feather_rows, output)
|
|
307
|
+
write_feather_rows(feather_rows, output, include_meta=include_meta)
|
|
286
308
|
|
|
287
309
|
if write_to_file:
|
|
288
310
|
click.echo(f"Wrote {output}", err=True)
|
|
@@ -0,0 +1,108 @@
|
|
|
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 _normalize_value(value: object) -> str:
|
|
11
|
+
if isinstance(value, (dict, list)):
|
|
12
|
+
return json.dumps(value)
|
|
13
|
+
if value is None:
|
|
14
|
+
return ""
|
|
15
|
+
return str(value)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_delta_rows(
|
|
19
|
+
delta: dict, *, include_meta: bool = False
|
|
20
|
+
) -> list[tuple[str, ...]]:
|
|
21
|
+
"""Flatten a single delta message into rows.
|
|
22
|
+
|
|
23
|
+
Without `include_meta`, rows are (timestamp, context, source, path,
|
|
24
|
+
value) from each update's "values" entries. With `include_meta`, a
|
|
25
|
+
"kind" column ("value"/"meta") is inserted before "value", and each
|
|
26
|
+
update's "meta" entries are included too — per the Streaming API spec,
|
|
27
|
+
"meta" entries have the same path/value shape but "value" is a metadata
|
|
28
|
+
object (units, description, zones, etc.), not a telemetry reading.
|
|
29
|
+
"""
|
|
30
|
+
context = delta.get("context", "")
|
|
31
|
+
rows: list[tuple[str, ...]] = []
|
|
32
|
+
for update in delta.get("updates", []):
|
|
33
|
+
timestamp = update.get("timestamp", "")
|
|
34
|
+
source = update.get("$source") or json.dumps(update.get("source", {}))
|
|
35
|
+
for entry in update.get("values", []):
|
|
36
|
+
path = entry.get("path", "")
|
|
37
|
+
value = _normalize_value(entry.get("value"))
|
|
38
|
+
if include_meta:
|
|
39
|
+
rows.append((timestamp, context, source, path, "value", value))
|
|
40
|
+
else:
|
|
41
|
+
rows.append((timestamp, context, source, path, value))
|
|
42
|
+
if include_meta:
|
|
43
|
+
for entry in update.get("meta", []):
|
|
44
|
+
path = entry.get("path", "")
|
|
45
|
+
value = _normalize_value(entry.get("value"))
|
|
46
|
+
rows.append((timestamp, context, source, path, "meta", value))
|
|
47
|
+
return rows
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CSV_COLUMNS = ["timestamp", "context", "source", "path", "value"]
|
|
51
|
+
CSV_COLUMNS_WITH_KIND = ["timestamp", "context", "source", "path", "kind", "value"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _columns(include_meta: bool) -> list[str]:
|
|
55
|
+
return CSV_COLUMNS_WITH_KIND if include_meta else CSV_COLUMNS
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_csv_header(sink: IO[str], *, include_meta: bool = False) -> None:
|
|
59
|
+
csv.writer(sink).writerow(_columns(include_meta))
|
|
60
|
+
sink.flush()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_csv_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -> int:
|
|
64
|
+
"""Write one delta's rows as CSV lines. Returns the number of rows written."""
|
|
65
|
+
rows = extract_delta_rows(delta, include_meta=include_meta)
|
|
66
|
+
writer = csv.writer(sink)
|
|
67
|
+
for row in rows:
|
|
68
|
+
writer.writerow(row)
|
|
69
|
+
sink.flush()
|
|
70
|
+
return len(rows)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def write_json_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -> int:
|
|
74
|
+
"""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)
|
|
76
|
+
columns = _columns(include_meta)
|
|
77
|
+
for row in rows:
|
|
78
|
+
sink.write(json.dumps(dict(zip(columns, row))))
|
|
79
|
+
sink.write("\n")
|
|
80
|
+
sink.flush()
|
|
81
|
+
return len(rows)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def write_feather_rows(
|
|
85
|
+
rows: list[tuple[str, ...]], output: str, *, include_meta: bool = False
|
|
86
|
+
) -> int:
|
|
87
|
+
"""Write accumulated delta rows as Feather. Returns the number of rows written.
|
|
88
|
+
|
|
89
|
+
Unlike CSV/JSON, Feather cannot be appended to incrementally — callers must
|
|
90
|
+
buffer rows across messages and call this once at the end of the session.
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
import pyarrow as pa
|
|
94
|
+
from pyarrow import feather
|
|
95
|
+
except ImportError:
|
|
96
|
+
raise ImportError(
|
|
97
|
+
"pyarrow is required for Feather output: pip install 'signalk-cli[feather]'"
|
|
98
|
+
) from None
|
|
99
|
+
columns = _columns(include_meta)
|
|
100
|
+
row_columns = list(zip(*rows)) if rows else [()] * len(columns)
|
|
101
|
+
table = pa.table(
|
|
102
|
+
{
|
|
103
|
+
name: pa.array(values, type=pa.string())
|
|
104
|
+
for name, values in zip(columns, row_columns)
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
feather.write_feather(table, output)
|
|
108
|
+
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
|