signalk-cli 2.1.0__tar.gz → 2.2.1__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.1.0 → signalk_cli-2.2.1}/PKG-INFO +55 -14
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/README.md +51 -10
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/pyproject.toml +28 -5
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/pyproject.toml.orig +27 -5
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/history/cli.py +3 -1
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/net.py +12 -1
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/stream/cli.py +42 -7
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/stream/output.py +79 -6
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/__init__.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/__main__.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/history/__init__.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/history/__main__.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/history/history_api.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/history/output.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/stream/__init__.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/stream/__main__.py +0 -0
- {signalk_cli-2.1.0 → signalk_cli-2.2.1}/src/signalk_cli/stream/stream_api.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: signalk-cli
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
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
|
|
7
7
|
Author-email: jey burrows <jrb@rhizomatics.org.uk>
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
|
-
Classifier: Development Status ::
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
10
|
Classifier: Environment :: Console
|
|
11
11
|
Classifier: Natural Language :: English
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
@@ -16,10 +16,10 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Requires-Dist: click>=8.3.3
|
|
18
18
|
Requires-Dist: niquests[ws]>=2.28
|
|
19
|
-
Requires-Dist: zeroconf>=0.149.16,<0.
|
|
19
|
+
Requires-Dist: zeroconf>=0.149.16,<0.152.0
|
|
20
20
|
Requires-Dist: pyarrow>=17.0 ; extra == 'feather'
|
|
21
21
|
Requires-Python: >=3.13
|
|
22
|
-
Project-URL: Homepage, https://
|
|
22
|
+
Project-URL: Homepage, https://signalk-cli.rhizomatics.org.uk
|
|
23
23
|
Project-URL: Repository, https://github.com/rhizomatics/signalk-cli
|
|
24
24
|
Project-URL: Issues, https://github.com/rhizomatics/signalk-cli/issues
|
|
25
25
|
Project-URL: Changelog, https://github.com/rhizomatics/signalk-cli/blob/main/CHANGELOG.md
|
|
@@ -28,22 +28,40 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
# SignalK CLI
|
|
30
30
|
|
|
31
|
+
[](https://github.com/rhizomatics)
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/signalk-cli/)
|
|
34
|
+
[](https://github.com/rhizomatics/signalk-cli)
|
|
35
|
+
[](https://signalk-cli.rhizomatics.org.uk/developer/coverage/)
|
|
36
|
+

|
|
37
|
+
[](https://results.pre-commit.ci/latest/github/rhizomatics/signalk-cli/main)
|
|
38
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/pypi-publish.yml)
|
|
39
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/python-package.yml)
|
|
40
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/github-code-scanning/codeql)
|
|
41
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/dependabot/dependabot-updates)
|
|
42
|
+
[](https://boat-tech-directory.rhizomatics.org.uk)
|
|
43
|
+
|
|
31
44
|
Query and explore NMEA and other boat data from SignalK APIs using the command line, and export data as CSV, Apache Arrow Feather, or JSON.
|
|
32
45
|
|
|
33
46
|
APIs supported:
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
### [SignalK v2 History API](https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html).
|
|
49
|
+
|
|
50
|
+
- Commands available:
|
|
51
|
+
- `list-paths`
|
|
52
|
+
- `list-providers`
|
|
53
|
+
- `list-contexts`
|
|
54
|
+
- `query`
|
|
55
|
+
- `cardinality`
|
|
56
|
+
|
|
57
|
+
### [SignalK v1 Streaming API](https://signalk.org/specification/1.8.2/doc/streaming_api.html).
|
|
58
|
+
|
|
59
|
+
- Commands available:
|
|
60
|
+
- `deltas`
|
|
43
61
|
|
|
44
62
|
## Installation
|
|
45
63
|
|
|
46
|
-
`signalk-cli` is published to PyPi
|
|
64
|
+
`signalk-cli` is published to [PyPi](https://pypi.org/project/signalk-cli/)
|
|
47
65
|
|
|
48
66
|
Python is required to run this, version 3.13 or above. [uv](https://docs.astral.sh/uv/) is the recommended way to install the package ( and can install Python ) but is not required.
|
|
49
67
|
|
|
@@ -53,6 +71,18 @@ Python is required to run this, version 3.13 or above. [uv](https://docs.astral.
|
|
|
53
71
|
|
|
54
72
|
For Apache Arrow Feather export, use the optional dependency: ```pip install 'signalk-cli[feather]'```
|
|
55
73
|
|
|
74
|
+
### Pyodide / slim install
|
|
75
|
+
|
|
76
|
+
`zeroconf` (used only for mDNS host discovery) can't run in Pyodide, and is optional at runtime. Install without dependencies and add the rest explicitly:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import micropip
|
|
80
|
+
await micropip.install(["click", "niquests[ws]>=3.21.0"])
|
|
81
|
+
await micropip.install("signalk-cli", deps=False)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Without `zeroconf`, the host must be given with `--host` or `SIGNALK_HOST` (or come from the cache).
|
|
85
|
+
|
|
56
86
|
### Local Copy
|
|
57
87
|
|
|
58
88
|
Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).
|
|
@@ -118,6 +148,7 @@ python -m signalk_cli.history query [OPTIONS] PATH...
|
|
|
118
148
|
```
|
|
119
149
|
|
|
120
150
|
**PATH** arguments may be:
|
|
151
|
+
|
|
121
152
|
- **Literal paths** — e.g. `navigation.speedOverGround`
|
|
122
153
|
- **Regex / glob patterns** — any argument containing metacharacters (`*`, `.`, `[`, `(`, etc.) is matched against the server's `/paths` endpoint. Bare `*` is treated as a glob wildcard.
|
|
123
154
|
- **Inline path specs** — `path:method` or `path:method:param`, e.g. `navigation.speedOverGround:sma:5`. These pass through to the server unchanged.
|
|
@@ -486,9 +517,10 @@ interchangeable:
|
|
|
486
517
|
| `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
|
|
487
518
|
| `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
|
|
488
519
|
| `--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). |
|
|
520
|
+
| `--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
521
|
| `--no-header` | — | Suppress the CSV header row |
|
|
491
522
|
| `--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. |
|
|
523
|
+
| `--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
524
|
| `-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
525
|
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
494
526
|
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
@@ -500,6 +532,8 @@ interchangeable:
|
|
|
500
532
|
|
|
501
533
|
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
502
534
|
|
|
535
|
+
**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.
|
|
536
|
+
|
|
503
537
|
**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
538
|
|
|
505
539
|
#### Examples
|
|
@@ -536,6 +570,13 @@ python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feat
|
|
|
536
570
|
|
|
537
571
|
# Capture until Ctrl-C to a named Feather file
|
|
538
572
|
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow -o capture.feather 'navigation.*'
|
|
573
|
+
|
|
574
|
+
# Only updates from one sensor (substring match on $source)
|
|
575
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --source Teltonika 'navigation.*'
|
|
576
|
+
|
|
577
|
+
# Bare speed values from one sensor, piped straight into another tool
|
|
578
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --format values \
|
|
579
|
+
--source Teltonika --bare navigation.speedOverGround
|
|
539
580
|
```
|
|
540
581
|
|
|
541
582
|
---
|
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
# SignalK CLI
|
|
2
2
|
|
|
3
|
+
[](https://github.com/rhizomatics)
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/signalk-cli/)
|
|
6
|
+
[](https://github.com/rhizomatics/signalk-cli)
|
|
7
|
+
[](https://signalk-cli.rhizomatics.org.uk/developer/coverage/)
|
|
8
|
+

|
|
9
|
+
[](https://results.pre-commit.ci/latest/github/rhizomatics/signalk-cli/main)
|
|
10
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/pypi-publish.yml)
|
|
11
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/python-package.yml)
|
|
12
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/github-code-scanning/codeql)
|
|
13
|
+
[](https://github.com/rhizomatics/signalk-cli/actions/workflows/dependabot/dependabot-updates)
|
|
14
|
+
[](https://boat-tech-directory.rhizomatics.org.uk)
|
|
15
|
+
|
|
3
16
|
Query and explore NMEA and other boat data from SignalK APIs using the command line, and export data as CSV, Apache Arrow Feather, or JSON.
|
|
4
17
|
|
|
5
18
|
APIs supported:
|
|
6
19
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
### [SignalK v2 History API](https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html).
|
|
21
|
+
|
|
22
|
+
- Commands available:
|
|
23
|
+
- `list-paths`
|
|
24
|
+
- `list-providers`
|
|
25
|
+
- `list-contexts`
|
|
26
|
+
- `query`
|
|
27
|
+
- `cardinality`
|
|
28
|
+
|
|
29
|
+
### [SignalK v1 Streaming API](https://signalk.org/specification/1.8.2/doc/streaming_api.html).
|
|
30
|
+
|
|
31
|
+
- Commands available:
|
|
32
|
+
- `deltas`
|
|
15
33
|
|
|
16
34
|
## Installation
|
|
17
35
|
|
|
18
|
-
`signalk-cli` is published to PyPi
|
|
36
|
+
`signalk-cli` is published to [PyPi](https://pypi.org/project/signalk-cli/)
|
|
19
37
|
|
|
20
38
|
Python is required to run this, version 3.13 or above. [uv](https://docs.astral.sh/uv/) is the recommended way to install the package ( and can install Python ) but is not required.
|
|
21
39
|
|
|
@@ -25,6 +43,18 @@ Python is required to run this, version 3.13 or above. [uv](https://docs.astral.
|
|
|
25
43
|
|
|
26
44
|
For Apache Arrow Feather export, use the optional dependency: ```pip install 'signalk-cli[feather]'```
|
|
27
45
|
|
|
46
|
+
### Pyodide / slim install
|
|
47
|
+
|
|
48
|
+
`zeroconf` (used only for mDNS host discovery) can't run in Pyodide, and is optional at runtime. Install without dependencies and add the rest explicitly:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import micropip
|
|
52
|
+
await micropip.install(["click", "niquests[ws]>=3.21.0"])
|
|
53
|
+
await micropip.install("signalk-cli", deps=False)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Without `zeroconf`, the host must be given with `--host` or `SIGNALK_HOST` (or come from the cache).
|
|
57
|
+
|
|
28
58
|
### Local Copy
|
|
29
59
|
|
|
30
60
|
Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/).
|
|
@@ -90,6 +120,7 @@ python -m signalk_cli.history query [OPTIONS] PATH...
|
|
|
90
120
|
```
|
|
91
121
|
|
|
92
122
|
**PATH** arguments may be:
|
|
123
|
+
|
|
93
124
|
- **Literal paths** — e.g. `navigation.speedOverGround`
|
|
94
125
|
- **Regex / glob patterns** — any argument containing metacharacters (`*`, `.`, `[`, `(`, etc.) is matched against the server's `/paths` endpoint. Bare `*` is treated as a glob wildcard.
|
|
95
126
|
- **Inline path specs** — `path:method` or `path:method:param`, e.g. `navigation.speedOverGround:sma:5`. These pass through to the server unchanged.
|
|
@@ -458,9 +489,10 @@ interchangeable:
|
|
|
458
489
|
| `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
|
|
459
490
|
| `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
|
|
460
491
|
| `--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). |
|
|
492
|
+
| `--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
493
|
| `--no-header` | — | Suppress the CSV header row |
|
|
463
494
|
| `--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. |
|
|
495
|
+
| `--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
496
|
| `-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
497
|
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
466
498
|
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
@@ -472,6 +504,8 @@ interchangeable:
|
|
|
472
504
|
|
|
473
505
|
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
474
506
|
|
|
507
|
+
**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.
|
|
508
|
+
|
|
475
509
|
**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
510
|
|
|
477
511
|
#### Examples
|
|
@@ -508,6 +542,13 @@ python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feat
|
|
|
508
542
|
|
|
509
543
|
# Capture until Ctrl-C to a named Feather file
|
|
510
544
|
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow -o capture.feather 'navigation.*'
|
|
545
|
+
|
|
546
|
+
# Only updates from one sensor (substring match on $source)
|
|
547
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --source Teltonika 'navigation.*'
|
|
548
|
+
|
|
549
|
+
# Bare speed values from one sensor, piped straight into another tool
|
|
550
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --format values \
|
|
551
|
+
--source Teltonika --bare navigation.speedOverGround
|
|
511
552
|
```
|
|
512
553
|
|
|
513
554
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "signalk-cli"
|
|
3
|
-
version = "2.1
|
|
3
|
+
version = "2.2.1"
|
|
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,7 +13,7 @@ keywords = [
|
|
|
13
13
|
"boating",
|
|
14
14
|
]
|
|
15
15
|
classifiers = [
|
|
16
|
-
"Development Status ::
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
17
|
"Environment :: Console",
|
|
18
18
|
"Natural Language :: English",
|
|
19
19
|
"Intended Audience :: Developers",
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
dependencies = [
|
|
26
26
|
"click>=8.3.3",
|
|
27
27
|
"niquests[ws]>=2.28",
|
|
28
|
-
"zeroconf>=0.149.16,<0.
|
|
28
|
+
"zeroconf>=0.149.16,<0.152.0",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[[project.authors]]
|
|
@@ -36,7 +36,7 @@ email = "jrb@rhizomatics.org.uk"
|
|
|
36
36
|
feather = ["pyarrow>=17.0"]
|
|
37
37
|
|
|
38
38
|
[project.urls]
|
|
39
|
-
Homepage = "https://
|
|
39
|
+
Homepage = "https://signalk-cli.rhizomatics.org.uk"
|
|
40
40
|
Repository = "https://github.com/rhizomatics/signalk-cli"
|
|
41
41
|
Issues = "https://github.com/rhizomatics/signalk-cli/issues"
|
|
42
42
|
Changelog = "https://github.com/rhizomatics/signalk-cli/blob/main/CHANGELOG.md"
|
|
@@ -55,9 +55,23 @@ dev = [
|
|
|
55
55
|
"pytest-xdist",
|
|
56
56
|
"mypy",
|
|
57
57
|
"coverage",
|
|
58
|
+
"genbadge[all]",
|
|
58
59
|
"icdiff",
|
|
59
60
|
"codespell",
|
|
60
61
|
]
|
|
62
|
+
docs = [
|
|
63
|
+
"properdocs",
|
|
64
|
+
"mkdocs-materialx[imaging]",
|
|
65
|
+
"mkdocs-minify-plugin",
|
|
66
|
+
"mkdocs-autorefs",
|
|
67
|
+
"mkdocs-pagetree-plugin",
|
|
68
|
+
"mkdocs-coverage",
|
|
69
|
+
"pymdown-extensions",
|
|
70
|
+
"mkdocs-meta-descriptions-plugin",
|
|
71
|
+
"pngquant",
|
|
72
|
+
"mkdocs-htmlproofer-plugin",
|
|
73
|
+
"mkdocs-llmstxt>=0.5.0,<0.6.0",
|
|
74
|
+
]
|
|
61
75
|
|
|
62
76
|
[tool.bandit]
|
|
63
77
|
exclude_dirs = ["tests"]
|
|
@@ -73,7 +87,6 @@ pythonpath = [
|
|
|
73
87
|
"src",
|
|
74
88
|
".",
|
|
75
89
|
]
|
|
76
|
-
asyncio_mode = "auto"
|
|
77
90
|
testpaths = ["tests"]
|
|
78
91
|
norecursedirs = [".git"]
|
|
79
92
|
|
|
@@ -99,6 +112,16 @@ ignore-words-list = "hass,referer,uint"
|
|
|
99
112
|
[tool.mypy]
|
|
100
113
|
mypy_path = ["./src"]
|
|
101
114
|
|
|
115
|
+
[[tool.mypy.overrides]]
|
|
116
|
+
module = "pyarrow.*"
|
|
117
|
+
ignore_missing_imports = true
|
|
118
|
+
|
|
119
|
+
[tool.ty.analysis]
|
|
120
|
+
allowed-unresolved-imports = [
|
|
121
|
+
"pyarrow",
|
|
122
|
+
"pyarrow.**",
|
|
123
|
+
]
|
|
124
|
+
|
|
102
125
|
[tool.uv]
|
|
103
126
|
compile-bytecode = true
|
|
104
127
|
managed = true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "signalk-cli"
|
|
3
|
-
version = "2.1
|
|
3
|
+
version = "2.2.1"
|
|
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"
|
|
@@ -11,7 +11,7 @@ authors = [
|
|
|
11
11
|
|
|
12
12
|
keywords = ["signalk", "sailing", "marine", "nmea","boating"]
|
|
13
13
|
classifiers = [
|
|
14
|
-
"Development Status ::
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
15
|
"Environment :: Console",
|
|
16
16
|
"Natural Language :: English",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
dependencies = [
|
|
25
25
|
"click>=8.3.3",
|
|
26
26
|
"niquests[ws]>=2.28",
|
|
27
|
-
"zeroconf>=0.149.16,<0.
|
|
27
|
+
"zeroconf>=0.149.16,<0.152.0",
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
[project.optional-dependencies]
|
|
@@ -39,12 +39,27 @@ dev = [
|
|
|
39
39
|
"pytest-xdist",
|
|
40
40
|
"mypy",
|
|
41
41
|
"coverage",
|
|
42
|
+
"genbadge[all]",
|
|
42
43
|
"icdiff",
|
|
43
44
|
"codespell"
|
|
44
45
|
]
|
|
45
46
|
|
|
47
|
+
docs=[
|
|
48
|
+
"properdocs",
|
|
49
|
+
"mkdocs-materialx[imaging]",
|
|
50
|
+
"mkdocs-minify-plugin",
|
|
51
|
+
"mkdocs-autorefs",
|
|
52
|
+
"mkdocs-pagetree-plugin",
|
|
53
|
+
"mkdocs-coverage",
|
|
54
|
+
"pymdown-extensions",
|
|
55
|
+
"mkdocs-meta-descriptions-plugin",
|
|
56
|
+
"pngquant",
|
|
57
|
+
"mkdocs-htmlproofer-plugin",
|
|
58
|
+
"mkdocs-llmstxt>=0.5.0,<0.6.0",
|
|
59
|
+
]
|
|
60
|
+
|
|
46
61
|
[project.urls]
|
|
47
|
-
Homepage = "https://
|
|
62
|
+
Homepage = "https://signalk-cli.rhizomatics.org.uk"
|
|
48
63
|
Repository = "https://github.com/rhizomatics/signalk-cli"
|
|
49
64
|
Issues="https://github.com/rhizomatics/signalk-cli/issues"
|
|
50
65
|
Changelog="https://github.com/rhizomatics/signalk-cli/blob/main/CHANGELOG.md"
|
|
@@ -67,7 +82,6 @@ line-ending = "auto"
|
|
|
67
82
|
|
|
68
83
|
[tool.pytest.ini_options]
|
|
69
84
|
pythonpath = ["src", "."]
|
|
70
|
-
asyncio_mode = "auto"
|
|
71
85
|
testpaths = [
|
|
72
86
|
"tests",
|
|
73
87
|
]
|
|
@@ -98,6 +112,14 @@ ignore-words-list="hass,referer,uint"
|
|
|
98
112
|
[tool.mypy]
|
|
99
113
|
mypy_path=["./src"]
|
|
100
114
|
|
|
115
|
+
[[tool.mypy.overrides]]
|
|
116
|
+
module = "pyarrow.*"
|
|
117
|
+
ignore_missing_imports = true
|
|
118
|
+
|
|
119
|
+
[tool.ty.analysis]
|
|
120
|
+
# optional `feather` extra
|
|
121
|
+
allowed-unresolved-imports = ["pyarrow", "pyarrow.**"]
|
|
122
|
+
|
|
101
123
|
[tool.uv]
|
|
102
124
|
compile-bytecode = true
|
|
103
125
|
managed = true
|
|
@@ -396,7 +396,9 @@ def query(
|
|
|
396
396
|
)
|
|
397
397
|
|
|
398
398
|
elif fmt == "raw":
|
|
399
|
-
raw_text =
|
|
399
|
+
raw_text = (
|
|
400
|
+
json.dumps(resp.json(), indent=indent) if pretty else (resp.text or "")
|
|
401
|
+
)
|
|
400
402
|
fh = _open_sink()
|
|
401
403
|
try:
|
|
402
404
|
(fh or sys.stdout).write(raw_text)
|
|
@@ -7,7 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
import niquests
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf
|
|
13
|
+
except ImportError: # slim/Pyodide installs ship without zeroconf
|
|
14
|
+
Zeroconf = None # type: ignore[assignment,misc]
|
|
11
15
|
|
|
12
16
|
CACHE_DIR = Path.home() / ".cache" / "signalk-cli"
|
|
13
17
|
_SIGNALK_TYPE = "_signalk-ws._tcp.local."
|
|
@@ -33,6 +37,8 @@ def save_cached_host(host: str) -> None:
|
|
|
33
37
|
|
|
34
38
|
def discover_host(timeout: float = 5.0) -> str | None:
|
|
35
39
|
"""Browse mDNS for a SignalK server and return its base URL, or None."""
|
|
40
|
+
if Zeroconf is None:
|
|
41
|
+
return None
|
|
36
42
|
found: list[str] = []
|
|
37
43
|
|
|
38
44
|
def _on_change(
|
|
@@ -119,6 +125,11 @@ def resolve_host(host: str | None, no_cache: bool = False) -> str:
|
|
|
119
125
|
if cached:
|
|
120
126
|
click.echo(f"Using cached host: {cached}", err=True)
|
|
121
127
|
return cached
|
|
128
|
+
if Zeroconf is None:
|
|
129
|
+
raise click.UsageError(
|
|
130
|
+
"No host specified and mDNS discovery unavailable (zeroconf not "
|
|
131
|
+
"installed). Use --host or set SIGNALK_HOST."
|
|
132
|
+
)
|
|
122
133
|
click.echo("No host specified — searching for SignalK via mDNS...", err=True)
|
|
123
134
|
discovered = discover_host()
|
|
124
135
|
if not discovered:
|
|
@@ -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(
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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
|