marlin-py 0.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.
- marlin_py-0.1.0/Cargo.toml +44 -0
- marlin_py-0.1.0/PKG-INFO +213 -0
- marlin_py-0.1.0/README.md +173 -0
- marlin_py-0.1.0/bindings/python/.gitignore +15 -0
- marlin_py-0.1.0/bindings/python/CHANGELOG.md +30 -0
- marlin_py-0.1.0/bindings/python/Cargo.lock +248 -0
- marlin_py-0.1.0/bindings/python/Cargo.toml +41 -0
- marlin_py-0.1.0/bindings/python/GUIDE.md +263 -0
- marlin_py-0.1.0/bindings/python/README.md +173 -0
- marlin_py-0.1.0/bindings/python/examples/decode_aivdm_log.py +45 -0
- marlin_py-0.1.0/bindings/python/examples/live_ais_dashboard.py +101 -0
- marlin_py-0.1.0/bindings/python/examples/one_shot_no_crlf.py +24 -0
- marlin_py-0.1.0/bindings/python/examples/parse_log_file.py +28 -0
- marlin_py-0.1.0/bindings/python/examples/parse_stdin.py +31 -0
- marlin_py-0.1.0/bindings/python/examples/streaming_tcp_style.py +45 -0
- marlin_py-0.1.0/bindings/python/pyrightconfig.json +9 -0
- marlin_py-0.1.0/bindings/python/src/ais.rs +1445 -0
- marlin_py-0.1.0/bindings/python/src/envelope.rs +336 -0
- marlin_py-0.1.0/bindings/python/src/errors.rs +90 -0
- marlin_py-0.1.0/bindings/python/src/lib.rs +20 -0
- marlin_py-0.1.0/bindings/python/src/nmea.rs +1303 -0
- marlin_py-0.1.0/bindings/python/tests/conftest.py +11 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/README.md +11 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/01_gga_basic.nmea +1 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/02_hdt_no_terminator.nmea +1 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/03_aivdm_encapsulation.nmea +1 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/04_rmc_lowercase_checksum.nmea +1 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/05_stream_mixed_terminators.nmea +3 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/06_tagged_sentence.nmea +1 -0
- marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/07_streaming_with_garbage.nmea +0 -0
- marlin_py-0.1.0/bindings/python/tests/golden/__init__.py +0 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/01_gga_basic.json +1 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/02_hdt_no_terminator.json +1 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/03_aivdm_encapsulation.json +29 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/04_rmc_lowercase_checksum.json +1 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/05_stream_mixed_terminators.json +1 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/06_tagged_sentence.json +1 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/07_streaming_with_garbage.json +1 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/01_gga_basic.json +60 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/02_hdt_no_terminator.json +24 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/03_aivdm_encapsulation.json +36 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/04_rmc_lowercase_checksum.json +51 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/05_stream_mixed_terminators.json +92 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/06_tagged_sentence.json +62 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/07_streaming_with_garbage.json +43 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/01_gga_basic.json +27 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/02_hdt_no_terminator.json +9 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/03_aivdm_encapsulation.json +9 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/04_rmc_lowercase_checksum.json +9 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/05_stream_mixed_terminators.json +23 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/06_tagged_sentence.json +27 -0
- marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/07_streaming_with_garbage.json +9 -0
- marlin_py-0.1.0/bindings/python/tests/golden/test_ais_fixtures.py +81 -0
- marlin_py-0.1.0/bindings/python/tests/golden/test_envelope_fixtures.py +65 -0
- marlin_py-0.1.0/bindings/python/tests/golden/test_nmea_fixtures.py +81 -0
- marlin_py-0.1.0/bindings/python/tests/properties/__init__.py +0 -0
- marlin_py-0.1.0/bindings/python/tests/properties/test_panic_freedom.py +51 -0
- marlin_py-0.1.0/bindings/python/tests/test_errors.py +22 -0
- marlin_py-0.1.0/bindings/python/tests/test_import.py +8 -0
- marlin_py-0.1.0/bindings/python/tests/unit/__init__.py +0 -0
- marlin_py-0.1.0/bindings/python/tests/unit/test_aio.py +136 -0
- marlin_py-0.1.0/bindings/python/tests/unit/test_ais.py +431 -0
- marlin_py-0.1.0/bindings/python/tests/unit/test_context_managers.py +78 -0
- marlin_py-0.1.0/bindings/python/tests/unit/test_dataclasses.py +216 -0
- marlin_py-0.1.0/bindings/python/tests/unit/test_envelope.py +163 -0
- marlin_py-0.1.0/bindings/python/tests/unit/test_examples.py +62 -0
- marlin_py-0.1.0/bindings/python/tests/unit/test_nmea.py +314 -0
- marlin_py-0.1.0/crates/marlin-ais/CHANGELOG.md +57 -0
- marlin_py-0.1.0/crates/marlin-ais/Cargo.toml +23 -0
- marlin_py-0.1.0/crates/marlin-ais/README.md +40 -0
- marlin_py-0.1.0/crates/marlin-ais/src/aivdm.rs +270 -0
- marlin_py-0.1.0/crates/marlin-ais/src/armor.rs +223 -0
- marlin_py-0.1.0/crates/marlin-ais/src/bit_reader.rs +390 -0
- marlin_py-0.1.0/crates/marlin-ais/src/error.rs +65 -0
- marlin_py-0.1.0/crates/marlin-ais/src/extended_position_report_b.rs +343 -0
- marlin_py-0.1.0/crates/marlin-ais/src/lib.rs +82 -0
- marlin_py-0.1.0/crates/marlin-ais/src/message.rs +567 -0
- marlin_py-0.1.0/crates/marlin-ais/src/parser.rs +635 -0
- marlin_py-0.1.0/crates/marlin-ais/src/position_report_a.rs +627 -0
- marlin_py-0.1.0/crates/marlin-ais/src/position_report_b.rs +271 -0
- marlin_py-0.1.0/crates/marlin-ais/src/reassembly.rs +840 -0
- marlin_py-0.1.0/crates/marlin-ais/src/shared_types.rs +108 -0
- marlin_py-0.1.0/crates/marlin-ais/src/static_data_b.rs +304 -0
- marlin_py-0.1.0/crates/marlin-ais/src/static_voyage_a.rs +352 -0
- marlin_py-0.1.0/crates/marlin-ais/src/testing.rs +131 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/CHANGELOG.md +41 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/Cargo.toml +26 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/README.md +67 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/error.rs +57 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/lib.rs +196 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/message.rs +40 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/parser.rs +508 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/gga.rs +267 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/hdt.rs +90 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/prdid.rs +472 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/psxn.rs +646 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/utc_time.rs +141 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/vtg.rs +288 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences.rs +24 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/testing.rs +40 -0
- marlin_py-0.1.0/crates/marlin-nmea-0183/src/util.rs +206 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/CHANGELOG.md +34 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/Cargo.toml +29 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/README.md +91 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/error.rs +95 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/lib.rs +292 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/one_shot.rs +533 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/parser.rs +252 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/sentence.rs +84 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/source.rs +88 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/streaming.rs +768 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/src/testing.rs +149 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/01_gga_basic.nmea +1 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/02_hdt_no_terminator.nmea +1 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/03_aivdm_encapsulation.nmea +1 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/04_rmc_lowercase_checksum.nmea +1 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/05_stream_mixed_terminators.nmea +3 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/06_tagged_sentence.nmea +1 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/07_streaming_with_garbage.nmea +0 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/README.md +41 -0
- marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/golden.rs +185 -0
- marlin_py-0.1.0/pyproject.toml +82 -0
- marlin_py-0.1.0/python/marlin/__init__.py +24 -0
- marlin_py-0.1.0/python/marlin/__init__.pyi +7 -0
- marlin_py-0.1.0/python/marlin/_core.pyi +748 -0
- marlin_py-0.1.0/python/marlin/aio.py +62 -0
- marlin_py-0.1.0/python/marlin/ais/__init__.py +55 -0
- marlin_py-0.1.0/python/marlin/ais/__init__.pyi +415 -0
- marlin_py-0.1.0/python/marlin/dataclasses.py +669 -0
- marlin_py-0.1.0/python/marlin/envelope/__init__.py +17 -0
- marlin_py-0.1.0/python/marlin/envelope/__init__.pyi +72 -0
- marlin_py-0.1.0/python/marlin/nmea/__init__.py +57 -0
- marlin_py-0.1.0/python/marlin/nmea/__init__.pyi +301 -0
- marlin_py-0.1.0/python/marlin/py.typed +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
resolver = "2"
|
|
3
|
+
members = [
|
|
4
|
+
"crates/marlin-nmea-envelope",
|
|
5
|
+
"crates/marlin-nmea-0183",
|
|
6
|
+
"crates/marlin-ais",
|
|
7
|
+
]
|
|
8
|
+
# The fuzz crate is not part of the main workspace because it requires
|
|
9
|
+
# nightly rustc (cargo-fuzz uses unstable sanitizers). Run it via
|
|
10
|
+
# `cargo +nightly fuzz run <target>`.
|
|
11
|
+
exclude = ["fuzz", "bindings/python"]
|
|
12
|
+
|
|
13
|
+
[workspace.package]
|
|
14
|
+
version = "0.1.0"
|
|
15
|
+
edition = "2021"
|
|
16
|
+
rust-version = "1.82"
|
|
17
|
+
license = "MIT OR Apache-2.0"
|
|
18
|
+
authors = ["Jurdanas"]
|
|
19
|
+
repository = "https://github.com/jkr78/marlin"
|
|
20
|
+
readme = "README.md"
|
|
21
|
+
|
|
22
|
+
[workspace.dependencies]
|
|
23
|
+
nom = { version = "8", default-features = false, features = ["alloc"] }
|
|
24
|
+
bytes = "1"
|
|
25
|
+
thiserror = "2"
|
|
26
|
+
tracing = { version = "0.1", default-features = false }
|
|
27
|
+
|
|
28
|
+
[workspace.lints.rust]
|
|
29
|
+
unsafe_code = "forbid"
|
|
30
|
+
missing_docs = "warn"
|
|
31
|
+
|
|
32
|
+
[workspace.lints.clippy]
|
|
33
|
+
pedantic = { level = "warn", priority = -1 }
|
|
34
|
+
unwrap_used = "deny"
|
|
35
|
+
expect_used = "deny"
|
|
36
|
+
panic = "deny"
|
|
37
|
+
indexing_slicing = "deny"
|
|
38
|
+
module_name_repetitions = "allow"
|
|
39
|
+
must_use_candidate = "allow"
|
|
40
|
+
missing_errors_doc = "allow"
|
|
41
|
+
|
|
42
|
+
[profile.release]
|
|
43
|
+
lto = "thin"
|
|
44
|
+
codegen-units = 1
|
marlin_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marlin-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Framework :: AsyncIO
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
+
Classifier: Programming Language :: Rust
|
|
18
|
+
Classifier: Topic :: Communications
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Networking
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Dist: pytest>=7 ; extra == 'dev'
|
|
25
|
+
Requires-Dist: hypothesis>=6 ; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.8 ; extra == 'dev'
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Summary: Sans-I/O NMEA 0183 + AIS (AIVDM/AIVDO) parser, Rust-backed
|
|
29
|
+
Keywords: nmea,nmea-0183,ais,aivdm,aivdo,marine,maritime,gps,parser,sans-io
|
|
30
|
+
Author: Jurdanas
|
|
31
|
+
License: MIT OR Apache-2.0
|
|
32
|
+
Requires-Python: >=3.9
|
|
33
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
34
|
+
Project-URL: Changelog, https://github.com/jkr78/marlin/blob/main/bindings/python/CHANGELOG.md
|
|
35
|
+
Project-URL: Documentation, https://github.com/jkr78/marlin/tree/main/bindings/python
|
|
36
|
+
Project-URL: Homepage, https://github.com/jkr78/marlin
|
|
37
|
+
Project-URL: Issues, https://github.com/jkr78/marlin/issues
|
|
38
|
+
Project-URL: Repository, https://github.com/jkr78/marlin
|
|
39
|
+
|
|
40
|
+
# marlin-py
|
|
41
|
+
|
|
42
|
+
Python bindings for the Marlin Rust suite — NMEA 0183 envelope parsing,
|
|
43
|
+
typed sentence decoding, and AIS (AIVDM/AIVDO) message decoding with
|
|
44
|
+
multi-sentence reassembly.
|
|
45
|
+
|
|
46
|
+
## Status
|
|
47
|
+
|
|
48
|
+
Wraps three Rust crates: `marlin-nmea-envelope` (framing + checksum),
|
|
49
|
+
`marlin-nmea-0183` (GGA, VTG, HDT, PSXN, PRDID typed decoders), and
|
|
50
|
+
`marlin-ais` (Types 1/2/3/5/18/19/24A/24B + reassembly). API surface is
|
|
51
|
+
feature-complete for v0.1. `py.typed` marker and `.pyi` stubs ship with the
|
|
52
|
+
package; mypy `--strict` is clean across the entire `python/marlin/`, `tests/`, and `examples/` tree.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
The PyPI distribution name is `marlin-py`; the Python import name stays
|
|
57
|
+
plain `marlin`.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# From PyPI (once the v0.1.0 wheels publish)
|
|
61
|
+
pip install marlin-py
|
|
62
|
+
|
|
63
|
+
# Local development — build the extension in-place
|
|
64
|
+
cd bindings/python
|
|
65
|
+
maturin develop
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quickstart: envelope
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from marlin.envelope import StreamingParser
|
|
72
|
+
|
|
73
|
+
parser = StreamingParser()
|
|
74
|
+
parser.feed(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n")
|
|
75
|
+
for sentence in parser:
|
|
76
|
+
talker = sentence.talker.decode() if sentence.talker else ""
|
|
77
|
+
print(talker, sentence.sentence_type, sentence.checksum_ok)
|
|
78
|
+
# GP GGA True
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`OneShotParser` handles UDP datagrams (no `\r\n` required). `parse()` is a
|
|
82
|
+
one-call convenience for a single complete sentence.
|
|
83
|
+
|
|
84
|
+
## Quickstart: NMEA typed decode
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from marlin.envelope import parse
|
|
88
|
+
from marlin.nmea import Nmea0183Parser, decode_gga, DecodeOptions, PrdidDialect
|
|
89
|
+
|
|
90
|
+
# Single-sentence convenience.
|
|
91
|
+
raw = parse(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47") # raises EnvelopeError on framing/checksum failure
|
|
92
|
+
gga = decode_gga(raw) # raises DecodeError if not GGA or fields are malformed
|
|
93
|
+
print(gga.latitude_deg, gga.longitude_deg, gga.fix_quality)
|
|
94
|
+
|
|
95
|
+
# Streaming parser — same feed/iterate API as the envelope layer.
|
|
96
|
+
parser = Nmea0183Parser.streaming()
|
|
97
|
+
parser.feed(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n")
|
|
98
|
+
for msg in parser:
|
|
99
|
+
print(type(msg).__name__, msg)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`DecodeOptions` configures dialect-sensitive sentences:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
opts = DecodeOptions().with_prdid_dialect(PrdidDialect.PITCH_ROLL_HEADING)
|
|
106
|
+
parser = Nmea0183Parser.streaming(options=opts)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Per-sentence extension functions (`decode_gga`, `decode_vtg`, `decode_hdt`,
|
|
110
|
+
`decode_psxn`, `decode_prdid`) are public so downstream code can build its
|
|
111
|
+
own message enum and delegate to Marlin for the standard types.
|
|
112
|
+
|
|
113
|
+
## Quickstart: AIS
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from marlin.ais import AisParser, PositionReportA
|
|
117
|
+
|
|
118
|
+
parser = AisParser.streaming()
|
|
119
|
+
parser.feed(b"!AIVDM,1,1,,A,13aGmP0P00PD;88MD5MTDww@2<0L,0*23\r\n")
|
|
120
|
+
for msg in parser:
|
|
121
|
+
if isinstance(msg.body, PositionReportA):
|
|
122
|
+
print(msg.body.mmsi, msg.body.latitude_deg, msg.body.longitude_deg)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Multi-sentence reassembly is automatic — feed fragments in order and the
|
|
126
|
+
parser yields a complete `AisMessage` only when the last fragment arrives.
|
|
127
|
+
|
|
128
|
+
## AIS clock modes
|
|
129
|
+
|
|
130
|
+
Fragment reassembly has three timeout behaviours, selected at construction.
|
|
131
|
+
|
|
132
|
+
**No timeout** — fragments are buffered until the 16-slot eviction cap is
|
|
133
|
+
reached. The oldest incomplete group is discarded when a 17th arrives. No
|
|
134
|
+
clock reads occur:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
parser = AisParser.streaming() # timeout_ms omitted → no timeout
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Auto clock** — the binding calls `time.monotonic_ns()` internally to
|
|
141
|
+
drive the timeout. Use this when system time is reliable and you do not
|
|
142
|
+
need deterministic replay:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
parser = AisParser.streaming(timeout_ms=60_000) # clock="auto" implied
|
|
146
|
+
# or explicitly:
|
|
147
|
+
parser = AisParser.streaming(timeout_ms=60_000, clock="auto")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Manual clock** — the caller drives the clock via `tick(now_ms=...)`. No
|
|
151
|
+
clock reads occur inside the binding. This is the correct mode for unit
|
|
152
|
+
tests, simulators, and any environment where `time` is patched:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
parser = AisParser.streaming(timeout_ms=60_000, clock="manual")
|
|
156
|
+
|
|
157
|
+
parser.feed(fragment_bytes)
|
|
158
|
+
parser.tick(now_ms=current_time_ms) # expire stale groups at this instant
|
|
159
|
+
for msg in parser:
|
|
160
|
+
...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Calling `tick()` on a parser that wasn't built with `clock="manual"` raises
|
|
164
|
+
`ValueError` — including parsers built with no `timeout_ms` (which default to
|
|
165
|
+
`clock="auto"`).
|
|
166
|
+
|
|
167
|
+
## Errors
|
|
168
|
+
|
|
169
|
+
Every exception is a `MarlinError` subclass (importable from `marlin`):
|
|
170
|
+
|
|
171
|
+
- `EnvelopeError` — framing or checksum failure
|
|
172
|
+
- `DecodeError` — field-level decode failure in a typed NMEA sentence
|
|
173
|
+
- `AisError` — AIS armor or bit-level decode failure
|
|
174
|
+
- `ReassemblyError` — fragment reassembly violation (out-of-order, channel
|
|
175
|
+
mismatch, or timeout eviction)
|
|
176
|
+
|
|
177
|
+
Property tests (via Hypothesis) verify panic-freedom on arbitrary byte
|
|
178
|
+
inputs — no input can cause the binding to crash the interpreter.
|
|
179
|
+
|
|
180
|
+
## Examples
|
|
181
|
+
|
|
182
|
+
Six runnable scripts live in `bindings/python/examples/`:
|
|
183
|
+
|
|
184
|
+
| Script | What it shows |
|
|
185
|
+
| --- | --- |
|
|
186
|
+
| `one_shot_no_crlf.py` | Single-datagram framing without `\r\n` (`OneShotParser`) |
|
|
187
|
+
| `parse_log_file.py` | Disk-backed `.nmea` log replay (`StreamingParser`) |
|
|
188
|
+
| `streaming_tcp_style.py` | TCP-style chunked feed; sentences straddle `feed()` boundaries |
|
|
189
|
+
| `decode_aivdm_log.py` | Multi-fragment AIS reassembly: Type 1 + Type 5 |
|
|
190
|
+
| `parse_stdin.py` | Stdin pipe reader for live NMEA/AIS capture |
|
|
191
|
+
| `live_ais_dashboard.py` | Live ship tracker: envelope + AIS in one script, periodic refresh |
|
|
192
|
+
|
|
193
|
+
## Type stubs
|
|
194
|
+
|
|
195
|
+
The `py.typed` marker is included in the package. Every public class,
|
|
196
|
+
function, and constant has a `.pyi` stub. Downstream projects that run
|
|
197
|
+
`mypy --strict` get full type-check coverage without extra configuration.
|
|
198
|
+
|
|
199
|
+
## Rust crates
|
|
200
|
+
|
|
201
|
+
The Python bindings wrap three Rust crates:
|
|
202
|
+
[`marlin-nmea-envelope`](../../crates/marlin-nmea-envelope),
|
|
203
|
+
[`marlin-nmea-0183`](../../crates/marlin-nmea-0183),
|
|
204
|
+
[`marlin-ais`](../../crates/marlin-ais).
|
|
205
|
+
|
|
206
|
+
## MSRV / Python version
|
|
207
|
+
|
|
208
|
+
Rust 1.82. Python 3.9+.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
Dual-licensed under MIT OR Apache-2.0.
|
|
213
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# marlin-py
|
|
2
|
+
|
|
3
|
+
Python bindings for the Marlin Rust suite — NMEA 0183 envelope parsing,
|
|
4
|
+
typed sentence decoding, and AIS (AIVDM/AIVDO) message decoding with
|
|
5
|
+
multi-sentence reassembly.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
Wraps three Rust crates: `marlin-nmea-envelope` (framing + checksum),
|
|
10
|
+
`marlin-nmea-0183` (GGA, VTG, HDT, PSXN, PRDID typed decoders), and
|
|
11
|
+
`marlin-ais` (Types 1/2/3/5/18/19/24A/24B + reassembly). API surface is
|
|
12
|
+
feature-complete for v0.1. `py.typed` marker and `.pyi` stubs ship with the
|
|
13
|
+
package; mypy `--strict` is clean across the entire `python/marlin/`, `tests/`, and `examples/` tree.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
The PyPI distribution name is `marlin-py`; the Python import name stays
|
|
18
|
+
plain `marlin`.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# From PyPI (once the v0.1.0 wheels publish)
|
|
22
|
+
pip install marlin-py
|
|
23
|
+
|
|
24
|
+
# Local development — build the extension in-place
|
|
25
|
+
cd bindings/python
|
|
26
|
+
maturin develop
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quickstart: envelope
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from marlin.envelope import StreamingParser
|
|
33
|
+
|
|
34
|
+
parser = StreamingParser()
|
|
35
|
+
parser.feed(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n")
|
|
36
|
+
for sentence in parser:
|
|
37
|
+
talker = sentence.talker.decode() if sentence.talker else ""
|
|
38
|
+
print(talker, sentence.sentence_type, sentence.checksum_ok)
|
|
39
|
+
# GP GGA True
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`OneShotParser` handles UDP datagrams (no `\r\n` required). `parse()` is a
|
|
43
|
+
one-call convenience for a single complete sentence.
|
|
44
|
+
|
|
45
|
+
## Quickstart: NMEA typed decode
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from marlin.envelope import parse
|
|
49
|
+
from marlin.nmea import Nmea0183Parser, decode_gga, DecodeOptions, PrdidDialect
|
|
50
|
+
|
|
51
|
+
# Single-sentence convenience.
|
|
52
|
+
raw = parse(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47") # raises EnvelopeError on framing/checksum failure
|
|
53
|
+
gga = decode_gga(raw) # raises DecodeError if not GGA or fields are malformed
|
|
54
|
+
print(gga.latitude_deg, gga.longitude_deg, gga.fix_quality)
|
|
55
|
+
|
|
56
|
+
# Streaming parser — same feed/iterate API as the envelope layer.
|
|
57
|
+
parser = Nmea0183Parser.streaming()
|
|
58
|
+
parser.feed(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n")
|
|
59
|
+
for msg in parser:
|
|
60
|
+
print(type(msg).__name__, msg)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`DecodeOptions` configures dialect-sensitive sentences:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
opts = DecodeOptions().with_prdid_dialect(PrdidDialect.PITCH_ROLL_HEADING)
|
|
67
|
+
parser = Nmea0183Parser.streaming(options=opts)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Per-sentence extension functions (`decode_gga`, `decode_vtg`, `decode_hdt`,
|
|
71
|
+
`decode_psxn`, `decode_prdid`) are public so downstream code can build its
|
|
72
|
+
own message enum and delegate to Marlin for the standard types.
|
|
73
|
+
|
|
74
|
+
## Quickstart: AIS
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from marlin.ais import AisParser, PositionReportA
|
|
78
|
+
|
|
79
|
+
parser = AisParser.streaming()
|
|
80
|
+
parser.feed(b"!AIVDM,1,1,,A,13aGmP0P00PD;88MD5MTDww@2<0L,0*23\r\n")
|
|
81
|
+
for msg in parser:
|
|
82
|
+
if isinstance(msg.body, PositionReportA):
|
|
83
|
+
print(msg.body.mmsi, msg.body.latitude_deg, msg.body.longitude_deg)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Multi-sentence reassembly is automatic — feed fragments in order and the
|
|
87
|
+
parser yields a complete `AisMessage` only when the last fragment arrives.
|
|
88
|
+
|
|
89
|
+
## AIS clock modes
|
|
90
|
+
|
|
91
|
+
Fragment reassembly has three timeout behaviours, selected at construction.
|
|
92
|
+
|
|
93
|
+
**No timeout** — fragments are buffered until the 16-slot eviction cap is
|
|
94
|
+
reached. The oldest incomplete group is discarded when a 17th arrives. No
|
|
95
|
+
clock reads occur:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
parser = AisParser.streaming() # timeout_ms omitted → no timeout
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Auto clock** — the binding calls `time.monotonic_ns()` internally to
|
|
102
|
+
drive the timeout. Use this when system time is reliable and you do not
|
|
103
|
+
need deterministic replay:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
parser = AisParser.streaming(timeout_ms=60_000) # clock="auto" implied
|
|
107
|
+
# or explicitly:
|
|
108
|
+
parser = AisParser.streaming(timeout_ms=60_000, clock="auto")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Manual clock** — the caller drives the clock via `tick(now_ms=...)`. No
|
|
112
|
+
clock reads occur inside the binding. This is the correct mode for unit
|
|
113
|
+
tests, simulators, and any environment where `time` is patched:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
parser = AisParser.streaming(timeout_ms=60_000, clock="manual")
|
|
117
|
+
|
|
118
|
+
parser.feed(fragment_bytes)
|
|
119
|
+
parser.tick(now_ms=current_time_ms) # expire stale groups at this instant
|
|
120
|
+
for msg in parser:
|
|
121
|
+
...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Calling `tick()` on a parser that wasn't built with `clock="manual"` raises
|
|
125
|
+
`ValueError` — including parsers built with no `timeout_ms` (which default to
|
|
126
|
+
`clock="auto"`).
|
|
127
|
+
|
|
128
|
+
## Errors
|
|
129
|
+
|
|
130
|
+
Every exception is a `MarlinError` subclass (importable from `marlin`):
|
|
131
|
+
|
|
132
|
+
- `EnvelopeError` — framing or checksum failure
|
|
133
|
+
- `DecodeError` — field-level decode failure in a typed NMEA sentence
|
|
134
|
+
- `AisError` — AIS armor or bit-level decode failure
|
|
135
|
+
- `ReassemblyError` — fragment reassembly violation (out-of-order, channel
|
|
136
|
+
mismatch, or timeout eviction)
|
|
137
|
+
|
|
138
|
+
Property tests (via Hypothesis) verify panic-freedom on arbitrary byte
|
|
139
|
+
inputs — no input can cause the binding to crash the interpreter.
|
|
140
|
+
|
|
141
|
+
## Examples
|
|
142
|
+
|
|
143
|
+
Six runnable scripts live in `bindings/python/examples/`:
|
|
144
|
+
|
|
145
|
+
| Script | What it shows |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `one_shot_no_crlf.py` | Single-datagram framing without `\r\n` (`OneShotParser`) |
|
|
148
|
+
| `parse_log_file.py` | Disk-backed `.nmea` log replay (`StreamingParser`) |
|
|
149
|
+
| `streaming_tcp_style.py` | TCP-style chunked feed; sentences straddle `feed()` boundaries |
|
|
150
|
+
| `decode_aivdm_log.py` | Multi-fragment AIS reassembly: Type 1 + Type 5 |
|
|
151
|
+
| `parse_stdin.py` | Stdin pipe reader for live NMEA/AIS capture |
|
|
152
|
+
| `live_ais_dashboard.py` | Live ship tracker: envelope + AIS in one script, periodic refresh |
|
|
153
|
+
|
|
154
|
+
## Type stubs
|
|
155
|
+
|
|
156
|
+
The `py.typed` marker is included in the package. Every public class,
|
|
157
|
+
function, and constant has a `.pyi` stub. Downstream projects that run
|
|
158
|
+
`mypy --strict` get full type-check coverage without extra configuration.
|
|
159
|
+
|
|
160
|
+
## Rust crates
|
|
161
|
+
|
|
162
|
+
The Python bindings wrap three Rust crates:
|
|
163
|
+
[`marlin-nmea-envelope`](../../crates/marlin-nmea-envelope),
|
|
164
|
+
[`marlin-nmea-0183`](../../crates/marlin-nmea-0183),
|
|
165
|
+
[`marlin-ais`](../../crates/marlin-ais).
|
|
166
|
+
|
|
167
|
+
## MSRV / Python version
|
|
168
|
+
|
|
169
|
+
Rust 1.82. Python 3.9+.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
Dual-licensed under MIT OR Apache-2.0.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Binding-local build artifacts. The crate is excluded from the workspace,
|
|
2
|
+
# so it gets its own Cargo.lock — treat it the same way fuzz/ does.
|
|
3
|
+
Cargo.lock
|
|
4
|
+
|
|
5
|
+
# uv's dev-env lockfile — downstream users install pre-built wheels, so this
|
|
6
|
+
# only pins the local pytest/maturin environment. Same library-vs-binary
|
|
7
|
+
# rationale as Cargo.lock above.
|
|
8
|
+
uv.lock
|
|
9
|
+
|
|
10
|
+
# Python bytecode + extension modules built in-place by `maturin develop`.
|
|
11
|
+
__pycache__/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
*.pyc
|
|
14
|
+
*.so
|
|
15
|
+
*.abi3.so
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `marlin-py` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-04-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release of `marlin-py` — Python bindings for the Marlin Rust suite
|
|
15
|
+
(envelope framing, NMEA 0183 typed decoders, AIS typed decoders + reassembly)
|
|
16
|
+
- Synchronous parsers: `OneShotParser`, `StreamingParser` (envelope),
|
|
17
|
+
`Nmea0183Parser` (NMEA), `AisParser` (AIS) — all with iterator protocol
|
|
18
|
+
- Context manager support (`with parser as p: ...`) on every parser
|
|
19
|
+
- Async iterator helpers in `marlin.aio`: `aiter_sentences`,
|
|
20
|
+
`aiter_nmea_messages`, `aiter_ais_messages` for `asyncio.StreamReader`
|
|
21
|
+
integration
|
|
22
|
+
- Frozen dataclass mirrors in `marlin.dataclasses` with `to_dataclass(msg)`
|
|
23
|
+
dispatcher — JSON / msgspec / dataclasses-asdict friendly
|
|
24
|
+
- Three AIS clock modes (no-timeout / auto / manual) with deterministic-replay
|
|
25
|
+
guarantee on `clock="manual"`
|
|
26
|
+
- Six runnable examples under `bindings/python/examples/`
|
|
27
|
+
- Type stubs (`py.typed` + `.pyi` files) for full mypy --strict coverage
|
|
28
|
+
- CI workflow: wheel builds for Linux x86_64/aarch64, macOS universal2,
|
|
29
|
+
Windows x86_64; pytest + mypy strict; sdist
|
|
30
|
+
- Hypothesis property tests verifying panic-freedom on arbitrary byte input
|