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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signalk-cli
3
- Version: 2.1.0
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 :: 4 - Beta
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.150.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://github.com/rhizomatics/signalk-cli
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
+ [![Rhizomatics Open Source](https://img.shields.io/badge/rhizomatics%20open%20source-lightseagreen)](https://github.com/rhizomatics)
32
+
33
+ [![PyPI - Version](https://img.shields.io/pypi/v/signalk-cli)](https://pypi.org/project/signalk-cli/)
34
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/rhizomatics/signalk-cli)
35
+ [![Coverage](https://raw.githubusercontent.com/rhizomatics/signalk-cli/refs/heads/badges/badges/coverage.svg)](https://signalk-cli.rhizomatics.org.uk/developer/coverage/)
36
+ ![Tests](https://raw.githubusercontent.com/rhizomatics/signalk-cli/refs/heads/badges/badges/tests.svg)
37
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/rhizomatics/signalk-cli/main.svg)](https://results.pre-commit.ci/latest/github/rhizomatics/signalk-cli/main)
38
+ [![Publish Python 🐍 distribution 📦 to PyPI and TestPyPI](https://github.com/rhizomatics/signalk-cli/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/rhizomatics/signalk-cli/actions/workflows/pypi-publish.yml)
39
+ [![Github Deploy](https://github.com/rhizomatics/signalk-cli/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/rhizomatics/signalk-cli/actions/workflows/python-package.yml)
40
+ [![CodeQL](https://github.com/rhizomatics/signalk-cli/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/rhizomatics/signalk-cli/actions/workflows/github-code-scanning/codeql)
41
+ [![Dependabot Updates](https://github.com/rhizomatics/signalk-cli/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/rhizomatics/signalk-cli/actions/workflows/dependabot/dependabot-updates)
42
+ [![boat tech directory](https://boat-tech-directory.rhizomatics.org.uk/images/badge.svg)](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
- * [SignalK v2 History API](https://signalk.org/https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html). Commands available:
36
- - list-paths
37
- - list-providers
38
- - list-contexts
39
- - query
40
- - cardinality
41
- * [SignalK v1 Streaming API](https://signalk.org/specification/1.8.2/doc/streaming_api.html). Commands available:
42
- - deltas
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 at https://pypi.org/project/signalk-cli/
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
+ [![Rhizomatics Open Source](https://img.shields.io/badge/rhizomatics%20open%20source-lightseagreen)](https://github.com/rhizomatics)
4
+
5
+ [![PyPI - Version](https://img.shields.io/pypi/v/signalk-cli)](https://pypi.org/project/signalk-cli/)
6
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/rhizomatics/signalk-cli)
7
+ [![Coverage](https://raw.githubusercontent.com/rhizomatics/signalk-cli/refs/heads/badges/badges/coverage.svg)](https://signalk-cli.rhizomatics.org.uk/developer/coverage/)
8
+ ![Tests](https://raw.githubusercontent.com/rhizomatics/signalk-cli/refs/heads/badges/badges/tests.svg)
9
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/rhizomatics/signalk-cli/main.svg)](https://results.pre-commit.ci/latest/github/rhizomatics/signalk-cli/main)
10
+ [![Publish Python 🐍 distribution 📦 to PyPI and TestPyPI](https://github.com/rhizomatics/signalk-cli/actions/workflows/pypi-publish.yml/badge.svg)](https://github.com/rhizomatics/signalk-cli/actions/workflows/pypi-publish.yml)
11
+ [![Github Deploy](https://github.com/rhizomatics/signalk-cli/actions/workflows/python-package.yml/badge.svg?branch=main)](https://github.com/rhizomatics/signalk-cli/actions/workflows/python-package.yml)
12
+ [![CodeQL](https://github.com/rhizomatics/signalk-cli/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/rhizomatics/signalk-cli/actions/workflows/github-code-scanning/codeql)
13
+ [![Dependabot Updates](https://github.com/rhizomatics/signalk-cli/actions/workflows/dependabot/dependabot-updates/badge.svg)](https://github.com/rhizomatics/signalk-cli/actions/workflows/dependabot/dependabot-updates)
14
+ [![boat tech directory](https://boat-tech-directory.rhizomatics.org.uk/images/badge.svg)](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
- * [SignalK v2 History API](https://signalk.org/https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html). Commands available:
8
- - list-paths
9
- - list-providers
10
- - list-contexts
11
- - query
12
- - cardinality
13
- * [SignalK v1 Streaming API](https://signalk.org/specification/1.8.2/doc/streaming_api.html). Commands available:
14
- - deltas
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 at https://pypi.org/project/signalk-cli/
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.0"
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 :: 4 - Beta",
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.150.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://github.com/rhizomatics/signalk-cli"
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.0"
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 :: 4 - Beta",
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.150.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://github.com/rhizomatics/signalk-cli"
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 = json.dumps(resp.json(), indent=indent) if pretty else resp.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
- from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf
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(["csv", "json", "raw", "feather"], case_sensitive=False),
108
+ type=click.Choice(
109
+ ["csv", "json", "raw", "feather", "values"], case_sensitive=False
110
+ ),
107
111
  help="Output format (default: inferred from --output extension, else csv). "
108
112
  "json is JSON Lines (one row object per line). raw is the exact delta "
109
- "message text, one per line. feather requires "
113
+ "message text, one per line. values is the bare value only, one per "
114
+ "line — no timestamp/context/source/path/kind columns. feather requires "
110
115
  "pip install 'signalk-cli[feather]' and --output (cannot stream to stdout).",
111
116
  )
112
117
  @click.option("--no-header", is_flag=True, help="Suppress header row (CSV only)")
@@ -117,6 +122,19 @@ def cli():
117
122
  "not just 'values'. Adds a 'kind' column (value/meta) to csv/json/feather "
118
123
  "output. Ignored for --format raw, which always includes meta as-is.",
119
124
  )
125
+ @click.option(
126
+ "--source",
127
+ "source",
128
+ multiple=True,
129
+ metavar="PATTERN",
130
+ help="Only include updates whose $source matches PATTERN. Repeatable "
131
+ "(OR'd together). PATTERN is a substring match unless it contains a "
132
+ "glob metacharacter (*/?/[), in which case it's matched as a glob, "
133
+ "e.g. --source Teltonika or --source '*.GP'. Filtering is client-side, "
134
+ "applied after receipt — for --format raw (whole message, verbatim) a "
135
+ "message passes if ANY of its updates match; other formats filter "
136
+ "per-update.",
137
+ )
120
138
  @click.option(
121
139
  "--output",
122
140
  "-o",
@@ -156,6 +174,7 @@ def deltas(
156
174
  fmt,
157
175
  no_header,
158
176
  include_meta,
177
+ source,
159
178
  output,
160
179
  follow,
161
180
  count,
@@ -164,7 +183,8 @@ def deltas(
164
183
  """Stream live delta updates from the SignalK v1 Streaming API.
165
184
 
166
185
  Connects via WebSocket and prints delta messages as they arrive, in
167
- csv, json (JSON Lines), raw, or Feather format.
186
+ csv, json (JSON Lines), raw, values (bare value only), or Feather
187
+ format.
168
188
 
169
189
  Always sends an explicit subscribe message for --context, covering
170
190
  PATH arguments if given, otherwise every path ('*'). PATH arguments
@@ -196,6 +216,10 @@ def deltas(
196
216
 
197
217
  # Capture 100 messages to a Feather file (requires signalk-cli[feather])
198
218
  signalk_cli.stream deltas --host 10.36.10.21 --count 100 -o capture.feather
219
+
220
+ # Bare speed values from one sensor, piped straight into another tool
221
+ signalk_cli.stream deltas --host 10.36.10.21 --follow --format values \\
222
+ --source Teltonika --bare navigation.speedOverGround
199
223
  """
200
224
  with stderr_ctx(bare):
201
225
  host = resolve_host(host, no_cache)
@@ -224,6 +248,8 @@ def deltas(
224
248
  if fmt == "feather"
225
249
  else ".json"
226
250
  if fmt in ("json", "raw")
251
+ else ".txt"
252
+ if fmt == "values"
227
253
  else ".csv"
228
254
  )
229
255
  output = f"signalk-stream-{server_name}-{ts}{ext}"
@@ -280,20 +306,29 @@ def deltas(
280
306
  message_count += 1
281
307
  if fmt == "feather":
282
308
  feather_rows.extend(
283
- extract_delta_rows(delta, include_meta=include_meta)
309
+ extract_delta_rows(
310
+ delta, include_meta=include_meta, sources=source
311
+ )
284
312
  )
285
313
  row_total = len(feather_rows)
286
314
  elif fmt == "raw":
287
- click.echo(raw, file=sink)
315
+ if delta_matches_source(delta, source):
316
+ click.echo(raw, file=sink)
288
317
  elif fmt == "json":
289
318
  row_total += write_json_delta(
290
- delta, sink, include_meta=include_meta
319
+ delta, sink, include_meta=include_meta, sources=source
320
+ )
321
+ elif fmt == "values":
322
+ row_total += write_values_delta(
323
+ delta, sink, include_meta=include_meta, sources=source
291
324
  )
292
325
  else:
293
326
  if not header_written and not no_header:
294
327
  write_csv_header(sink, include_meta=include_meta)
295
328
  header_written = True
296
- row_total += write_csv_delta(delta, sink, include_meta=include_meta)
329
+ row_total += write_csv_delta(
330
+ delta, sink, include_meta=include_meta, sources=source
331
+ )
297
332
  except KeyboardInterrupt:
298
333
  pass
299
334
  except niquests.RequestException as e:
@@ -1,6 +1,7 @@
1
1
  """Row extraction and CSV/JSON/Feather writers for SignalK delta messages."""
2
2
 
3
3
  import csv
4
+ import fnmatch
4
5
  import json
5
6
  from typing import IO
6
7
 
@@ -15,8 +16,42 @@ def _normalize_value(value: object) -> str:
15
16
  return str(value)
16
17
 
17
18
 
19
+ def _update_source(update: dict) -> str:
20
+ return update.get("$source") or json.dumps(update.get("source", {}))
21
+
22
+
23
+ def source_matches(source: str, patterns: tuple[str, ...]) -> bool:
24
+ """Match a `$source` string against `--source` filter patterns (OR'd).
25
+
26
+ No patterns means no filtering (always matches). A pattern containing
27
+ glob metacharacters (`*`/`?`/`[`) is matched as-is via `fnmatch`;
28
+ otherwise it's treated as a substring match, e.g. "Teltonika" matches
29
+ the source "Teltonika.GP".
30
+ """
31
+ if not patterns:
32
+ return True
33
+ return any(
34
+ fnmatch.fnmatch(source, p if any(c in p for c in "*?[") else f"*{p}*")
35
+ for p in patterns
36
+ )
37
+
38
+
39
+ def delta_matches_source(delta: dict, patterns: tuple[str, ...]) -> bool:
40
+ """True if any update in the delta has a `$source` matching `patterns`.
41
+
42
+ Used for `--format raw`, which echoes the whole message verbatim and so
43
+ can only filter at message granularity, not per-update.
44
+ """
45
+ if not patterns:
46
+ return True
47
+ return any(
48
+ source_matches(_update_source(update), patterns)
49
+ for update in delta.get("updates", [])
50
+ )
51
+
52
+
18
53
  def extract_delta_rows(
19
- delta: dict, *, include_meta: bool = False
54
+ delta: dict, *, include_meta: bool = False, sources: tuple[str, ...] = ()
20
55
  ) -> list[tuple[str, ...]]:
21
56
  """Flatten a single delta message into rows.
22
57
 
@@ -26,12 +61,18 @@ def extract_delta_rows(
26
61
  update's "meta" entries are included too — per the Streaming API spec,
27
62
  "meta" entries have the same path/value shape but "value" is a metadata
28
63
  object (units, description, zones, etc.), not a telemetry reading.
64
+
65
+ `sources`, if given, drops entire updates whose `$source` doesn't match
66
+ any pattern (see `source_matches`) — filtering is per-update, since
67
+ that's the granularity at which SignalK attaches a source.
29
68
  """
30
69
  context = delta.get("context", "")
31
70
  rows: list[tuple[str, ...]] = []
32
71
  for update in delta.get("updates", []):
72
+ source = _update_source(update)
73
+ if not source_matches(source, sources):
74
+ continue
33
75
  timestamp = update.get("timestamp", "")
34
- source = update.get("$source") or json.dumps(update.get("source", {}))
35
76
  for entry in update.get("values", []):
36
77
  path = entry.get("path", "")
37
78
  value = _normalize_value(entry.get("value"))
@@ -60,9 +101,15 @@ def write_csv_header(sink: IO[str], *, include_meta: bool = False) -> None:
60
101
  sink.flush()
61
102
 
62
103
 
63
- def write_csv_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -> int:
104
+ def write_csv_delta(
105
+ delta: dict,
106
+ sink: IO[str],
107
+ *,
108
+ include_meta: bool = False,
109
+ sources: tuple[str, ...] = (),
110
+ ) -> int:
64
111
  """Write one delta's rows as CSV lines. Returns the number of rows written."""
65
- rows = extract_delta_rows(delta, include_meta=include_meta)
112
+ rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
66
113
  writer = csv.writer(sink)
67
114
  for row in rows:
68
115
  writer.writerow(row)
@@ -70,9 +117,15 @@ def write_csv_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -
70
117
  return len(rows)
71
118
 
72
119
 
73
- def write_json_delta(delta: dict, sink: IO[str], *, include_meta: bool = False) -> int:
120
+ def write_json_delta(
121
+ delta: dict,
122
+ sink: IO[str],
123
+ *,
124
+ include_meta: bool = False,
125
+ sources: tuple[str, ...] = (),
126
+ ) -> int:
74
127
  """Write one delta's rows as JSON Lines (one row object per line). Returns row count."""
75
- rows = extract_delta_rows(delta, include_meta=include_meta)
128
+ rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
76
129
  columns = _columns(include_meta)
77
130
  for row in rows:
78
131
  sink.write(json.dumps(dict(zip(columns, row))))
@@ -81,6 +134,26 @@ def write_json_delta(delta: dict, sink: IO[str], *, include_meta: bool = False)
81
134
  return len(rows)
82
135
 
83
136
 
137
+ def write_values_delta(
138
+ delta: dict,
139
+ sink: IO[str],
140
+ *,
141
+ include_meta: bool = False,
142
+ sources: tuple[str, ...] = (),
143
+ ) -> int:
144
+ """Write one delta's bare values, one per line — no other columns.
145
+
146
+ For `--format values`: useful for piping a single path's readings
147
+ straight into another tool/script. Returns the number of values written.
148
+ """
149
+ rows = extract_delta_rows(delta, include_meta=include_meta, sources=sources)
150
+ for row in rows:
151
+ sink.write(row[-1])
152
+ sink.write("\n")
153
+ sink.flush()
154
+ return len(rows)
155
+
156
+
84
157
  def write_feather_rows(
85
158
  rows: list[tuple[str, ...]], output: str, *, include_meta: bool = False
86
159
  ) -> int: