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.
Files changed (134) hide show
  1. marlin_py-0.1.0/Cargo.toml +44 -0
  2. marlin_py-0.1.0/PKG-INFO +213 -0
  3. marlin_py-0.1.0/README.md +173 -0
  4. marlin_py-0.1.0/bindings/python/.gitignore +15 -0
  5. marlin_py-0.1.0/bindings/python/CHANGELOG.md +30 -0
  6. marlin_py-0.1.0/bindings/python/Cargo.lock +248 -0
  7. marlin_py-0.1.0/bindings/python/Cargo.toml +41 -0
  8. marlin_py-0.1.0/bindings/python/GUIDE.md +263 -0
  9. marlin_py-0.1.0/bindings/python/README.md +173 -0
  10. marlin_py-0.1.0/bindings/python/examples/decode_aivdm_log.py +45 -0
  11. marlin_py-0.1.0/bindings/python/examples/live_ais_dashboard.py +101 -0
  12. marlin_py-0.1.0/bindings/python/examples/one_shot_no_crlf.py +24 -0
  13. marlin_py-0.1.0/bindings/python/examples/parse_log_file.py +28 -0
  14. marlin_py-0.1.0/bindings/python/examples/parse_stdin.py +31 -0
  15. marlin_py-0.1.0/bindings/python/examples/streaming_tcp_style.py +45 -0
  16. marlin_py-0.1.0/bindings/python/pyrightconfig.json +9 -0
  17. marlin_py-0.1.0/bindings/python/src/ais.rs +1445 -0
  18. marlin_py-0.1.0/bindings/python/src/envelope.rs +336 -0
  19. marlin_py-0.1.0/bindings/python/src/errors.rs +90 -0
  20. marlin_py-0.1.0/bindings/python/src/lib.rs +20 -0
  21. marlin_py-0.1.0/bindings/python/src/nmea.rs +1303 -0
  22. marlin_py-0.1.0/bindings/python/tests/conftest.py +11 -0
  23. marlin_py-0.1.0/bindings/python/tests/fixtures/README.md +11 -0
  24. marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/01_gga_basic.nmea +1 -0
  25. marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/02_hdt_no_terminator.nmea +1 -0
  26. marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/03_aivdm_encapsulation.nmea +1 -0
  27. marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/04_rmc_lowercase_checksum.nmea +1 -0
  28. marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/05_stream_mixed_terminators.nmea +3 -0
  29. marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/06_tagged_sentence.nmea +1 -0
  30. marlin_py-0.1.0/bindings/python/tests/fixtures/envelope/07_streaming_with_garbage.nmea +0 -0
  31. marlin_py-0.1.0/bindings/python/tests/golden/__init__.py +0 -0
  32. marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/01_gga_basic.json +1 -0
  33. marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/02_hdt_no_terminator.json +1 -0
  34. marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/03_aivdm_encapsulation.json +29 -0
  35. marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/04_rmc_lowercase_checksum.json +1 -0
  36. marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/05_stream_mixed_terminators.json +1 -0
  37. marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/06_tagged_sentence.json +1 -0
  38. marlin_py-0.1.0/bindings/python/tests/golden/expected/ais/07_streaming_with_garbage.json +1 -0
  39. marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/01_gga_basic.json +60 -0
  40. marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/02_hdt_no_terminator.json +24 -0
  41. marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/03_aivdm_encapsulation.json +36 -0
  42. marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/04_rmc_lowercase_checksum.json +51 -0
  43. marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/05_stream_mixed_terminators.json +92 -0
  44. marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/06_tagged_sentence.json +62 -0
  45. marlin_py-0.1.0/bindings/python/tests/golden/expected/envelope/07_streaming_with_garbage.json +43 -0
  46. marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/01_gga_basic.json +27 -0
  47. marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/02_hdt_no_terminator.json +9 -0
  48. marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/03_aivdm_encapsulation.json +9 -0
  49. marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/04_rmc_lowercase_checksum.json +9 -0
  50. marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/05_stream_mixed_terminators.json +23 -0
  51. marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/06_tagged_sentence.json +27 -0
  52. marlin_py-0.1.0/bindings/python/tests/golden/expected/nmea/07_streaming_with_garbage.json +9 -0
  53. marlin_py-0.1.0/bindings/python/tests/golden/test_ais_fixtures.py +81 -0
  54. marlin_py-0.1.0/bindings/python/tests/golden/test_envelope_fixtures.py +65 -0
  55. marlin_py-0.1.0/bindings/python/tests/golden/test_nmea_fixtures.py +81 -0
  56. marlin_py-0.1.0/bindings/python/tests/properties/__init__.py +0 -0
  57. marlin_py-0.1.0/bindings/python/tests/properties/test_panic_freedom.py +51 -0
  58. marlin_py-0.1.0/bindings/python/tests/test_errors.py +22 -0
  59. marlin_py-0.1.0/bindings/python/tests/test_import.py +8 -0
  60. marlin_py-0.1.0/bindings/python/tests/unit/__init__.py +0 -0
  61. marlin_py-0.1.0/bindings/python/tests/unit/test_aio.py +136 -0
  62. marlin_py-0.1.0/bindings/python/tests/unit/test_ais.py +431 -0
  63. marlin_py-0.1.0/bindings/python/tests/unit/test_context_managers.py +78 -0
  64. marlin_py-0.1.0/bindings/python/tests/unit/test_dataclasses.py +216 -0
  65. marlin_py-0.1.0/bindings/python/tests/unit/test_envelope.py +163 -0
  66. marlin_py-0.1.0/bindings/python/tests/unit/test_examples.py +62 -0
  67. marlin_py-0.1.0/bindings/python/tests/unit/test_nmea.py +314 -0
  68. marlin_py-0.1.0/crates/marlin-ais/CHANGELOG.md +57 -0
  69. marlin_py-0.1.0/crates/marlin-ais/Cargo.toml +23 -0
  70. marlin_py-0.1.0/crates/marlin-ais/README.md +40 -0
  71. marlin_py-0.1.0/crates/marlin-ais/src/aivdm.rs +270 -0
  72. marlin_py-0.1.0/crates/marlin-ais/src/armor.rs +223 -0
  73. marlin_py-0.1.0/crates/marlin-ais/src/bit_reader.rs +390 -0
  74. marlin_py-0.1.0/crates/marlin-ais/src/error.rs +65 -0
  75. marlin_py-0.1.0/crates/marlin-ais/src/extended_position_report_b.rs +343 -0
  76. marlin_py-0.1.0/crates/marlin-ais/src/lib.rs +82 -0
  77. marlin_py-0.1.0/crates/marlin-ais/src/message.rs +567 -0
  78. marlin_py-0.1.0/crates/marlin-ais/src/parser.rs +635 -0
  79. marlin_py-0.1.0/crates/marlin-ais/src/position_report_a.rs +627 -0
  80. marlin_py-0.1.0/crates/marlin-ais/src/position_report_b.rs +271 -0
  81. marlin_py-0.1.0/crates/marlin-ais/src/reassembly.rs +840 -0
  82. marlin_py-0.1.0/crates/marlin-ais/src/shared_types.rs +108 -0
  83. marlin_py-0.1.0/crates/marlin-ais/src/static_data_b.rs +304 -0
  84. marlin_py-0.1.0/crates/marlin-ais/src/static_voyage_a.rs +352 -0
  85. marlin_py-0.1.0/crates/marlin-ais/src/testing.rs +131 -0
  86. marlin_py-0.1.0/crates/marlin-nmea-0183/CHANGELOG.md +41 -0
  87. marlin_py-0.1.0/crates/marlin-nmea-0183/Cargo.toml +26 -0
  88. marlin_py-0.1.0/crates/marlin-nmea-0183/README.md +67 -0
  89. marlin_py-0.1.0/crates/marlin-nmea-0183/src/error.rs +57 -0
  90. marlin_py-0.1.0/crates/marlin-nmea-0183/src/lib.rs +196 -0
  91. marlin_py-0.1.0/crates/marlin-nmea-0183/src/message.rs +40 -0
  92. marlin_py-0.1.0/crates/marlin-nmea-0183/src/parser.rs +508 -0
  93. marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/gga.rs +267 -0
  94. marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/hdt.rs +90 -0
  95. marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/prdid.rs +472 -0
  96. marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/psxn.rs +646 -0
  97. marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/utc_time.rs +141 -0
  98. marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences/vtg.rs +288 -0
  99. marlin_py-0.1.0/crates/marlin-nmea-0183/src/sentences.rs +24 -0
  100. marlin_py-0.1.0/crates/marlin-nmea-0183/src/testing.rs +40 -0
  101. marlin_py-0.1.0/crates/marlin-nmea-0183/src/util.rs +206 -0
  102. marlin_py-0.1.0/crates/marlin-nmea-envelope/CHANGELOG.md +34 -0
  103. marlin_py-0.1.0/crates/marlin-nmea-envelope/Cargo.toml +29 -0
  104. marlin_py-0.1.0/crates/marlin-nmea-envelope/README.md +91 -0
  105. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/error.rs +95 -0
  106. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/lib.rs +292 -0
  107. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/one_shot.rs +533 -0
  108. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/parser.rs +252 -0
  109. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/sentence.rs +84 -0
  110. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/source.rs +88 -0
  111. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/streaming.rs +768 -0
  112. marlin_py-0.1.0/crates/marlin-nmea-envelope/src/testing.rs +149 -0
  113. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/01_gga_basic.nmea +1 -0
  114. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/02_hdt_no_terminator.nmea +1 -0
  115. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/03_aivdm_encapsulation.nmea +1 -0
  116. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/04_rmc_lowercase_checksum.nmea +1 -0
  117. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/05_stream_mixed_terminators.nmea +3 -0
  118. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/06_tagged_sentence.nmea +1 -0
  119. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/07_streaming_with_garbage.nmea +0 -0
  120. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/fixtures/README.md +41 -0
  121. marlin_py-0.1.0/crates/marlin-nmea-envelope/tests/golden.rs +185 -0
  122. marlin_py-0.1.0/pyproject.toml +82 -0
  123. marlin_py-0.1.0/python/marlin/__init__.py +24 -0
  124. marlin_py-0.1.0/python/marlin/__init__.pyi +7 -0
  125. marlin_py-0.1.0/python/marlin/_core.pyi +748 -0
  126. marlin_py-0.1.0/python/marlin/aio.py +62 -0
  127. marlin_py-0.1.0/python/marlin/ais/__init__.py +55 -0
  128. marlin_py-0.1.0/python/marlin/ais/__init__.pyi +415 -0
  129. marlin_py-0.1.0/python/marlin/dataclasses.py +669 -0
  130. marlin_py-0.1.0/python/marlin/envelope/__init__.py +17 -0
  131. marlin_py-0.1.0/python/marlin/envelope/__init__.pyi +72 -0
  132. marlin_py-0.1.0/python/marlin/nmea/__init__.py +57 -0
  133. marlin_py-0.1.0/python/marlin/nmea/__init__.pyi +301 -0
  134. 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
@@ -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