capkit 0.1.0__tar.gz → 0.2.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 (43) hide show
  1. {capkit-0.1.0 → capkit-0.2.0}/.github/workflows/ci.yml +2 -0
  2. {capkit-0.1.0 → capkit-0.2.0}/CHANGELOG.md +17 -1
  3. {capkit-0.1.0 → capkit-0.2.0}/PKG-INFO +14 -7
  4. {capkit-0.1.0 → capkit-0.2.0}/README.md +12 -6
  5. capkit-0.2.0/ROADMAP.md +45 -0
  6. {capkit-0.1.0 → capkit-0.2.0}/capkit/model/frame.py +2 -0
  7. {capkit-0.1.0 → capkit-0.2.0}/capkit/readers/__init__.py +70 -8
  8. capkit-0.2.0/capkit/readers/candump.py +98 -0
  9. capkit-0.2.0/capkit/readers/vector_asc.py +300 -0
  10. {capkit-0.1.0 → capkit-0.2.0}/docs/api-reference.md +49 -10
  11. capkit-0.2.0/docs/format-support.md +117 -0
  12. {capkit-0.1.0 → capkit-0.2.0}/docs/releasing.md +7 -6
  13. {capkit-0.1.0 → capkit-0.2.0}/pyproject.toml +4 -1
  14. capkit-0.2.0/tests/fixtures/README.md +17 -0
  15. capkit-0.2.0/tests/fixtures/candump/candump.log +300 -0
  16. capkit-0.2.0/tests/fixtures/vector_asc/LICENSE.python-can.txt +165 -0
  17. capkit-0.2.0/tests/fixtures/vector_asc/python_can_logfile.asc +39 -0
  18. capkit-0.2.0/tests/test_candump.py +138 -0
  19. {capkit-0.1.0 → capkit-0.2.0}/tests/test_integration_dbckit.py +28 -0
  20. {capkit-0.1.0 → capkit-0.2.0}/tests/test_io.py +6 -2
  21. {capkit-0.1.0 → capkit-0.2.0}/tests/test_model.py +9 -0
  22. capkit-0.2.0/tests/test_reader_entry_points.py +165 -0
  23. capkit-0.2.0/tests/test_vector_asc.py +165 -0
  24. capkit-0.2.0/tests/test_vector_asc_crosscheck.py +41 -0
  25. capkit-0.1.0/ROADMAP.md +0 -71
  26. capkit-0.1.0/docs/format-support.md +0 -54
  27. {capkit-0.1.0 → capkit-0.2.0}/.github/workflows/release.yml +0 -0
  28. {capkit-0.1.0 → capkit-0.2.0}/.gitignore +0 -0
  29. {capkit-0.1.0 → capkit-0.2.0}/LICENSE +0 -0
  30. {capkit-0.1.0 → capkit-0.2.0}/capkit/__init__.py +0 -0
  31. {capkit-0.1.0 → capkit-0.2.0}/capkit/integration.py +0 -0
  32. {capkit-0.1.0 → capkit-0.2.0}/capkit/io.py +0 -0
  33. {capkit-0.1.0 → capkit-0.2.0}/capkit/model/__init__.py +0 -0
  34. {capkit-0.1.0 → capkit-0.2.0}/capkit/operations/__init__.py +0 -0
  35. {capkit-0.1.0 → capkit-0.2.0}/capkit/py.typed +0 -0
  36. {capkit-0.1.0 → capkit-0.2.0}/capkit/readers/base.py +0 -0
  37. {capkit-0.1.0 → capkit-0.2.0}/capkit/readers/kvaser_txt.py +0 -0
  38. {capkit-0.1.0 → capkit-0.2.0}/capkit/writers/__init__.py +0 -0
  39. {capkit-0.1.0 → capkit-0.2.0}/docs/recipes.md +0 -0
  40. {capkit-0.1.0 → capkit-0.2.0}/tests/__init__.py +0 -0
  41. {capkit-0.1.0 → capkit-0.2.0}/tests/fixtures/kvaser/kvaser.txt +0 -0
  42. {capkit-0.1.0 → capkit-0.2.0}/tests/test_contract.py +0 -0
  43. {capkit-0.1.0 → capkit-0.2.0}/tests/test_kvaser_txt.py +0 -0
@@ -60,4 +60,6 @@ jobs:
60
60
  entry=next(name for name in names if name.endswith('entry_points.txt'));
61
61
  text=archive.read(entry).decode();
62
62
  assert '[dbckit.readers]' in text;
63
+ assert 'asc = capkit.readers.vector_asc:VectorAscReader' in text;
64
+ assert 'log = capkit.integration:DispatchReader' in text;
63
65
  assert 'txt = capkit.integration:DispatchReader' in text"
@@ -7,7 +7,23 @@ Versioning will follow [Semantic Versioning](https://semver.org/) from 1.0.0 onw
7
7
 
8
8
  ---
9
9
 
10
- ## [Unreleased]
10
+ ## [0.2.0] — 2026-07-16
11
+
12
+ ### Added
13
+
14
+ - can-utils `candump -L` reader (`candump`) with classic, remote, and CAN FD support,
15
+ pinned by a 300-frame fixture.
16
+ - Vector CANalyzer/CANoe ASC reader (`vector-asc`) covering classic, remote, error,
17
+ and CAN FD rows plus header-derived capture start times.
18
+ - `Frame.bitrate_switch` and `Frame.error_state_indicator` CAN FD flags.
19
+ - Lazy, cached discovery of third-party readers through the `capkit.readers`
20
+ entry-point group, including validated conflict and failure reporting.
21
+ - python-can reference writer/reader cross-checks in the development test suite.
22
+
23
+ ### Changed
24
+
25
+ - dbckit integration now handles `.log` through the sniffing dispatcher and
26
+ supersedes dbckit's built-in `.asc` reader with capkit's richer implementation.
11
27
 
12
28
  ## [0.1.0] — 2026-07-16
13
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capkit
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Read CAN bus capture logs in different formats into one common frame stream
5
5
  Project-URL: Homepage, https://canforge.io/capkit
6
6
  Project-URL: Repository, https://github.com/canforge/capkit
@@ -44,6 +44,7 @@ Requires-Dist: dbckit>=1.0; extra == 'dev'
44
44
  Requires-Dist: mypy; extra == 'dev'
45
45
  Requires-Dist: pytest-cov; extra == 'dev'
46
46
  Requires-Dist: pytest>=8.0; extra == 'dev'
47
+ Requires-Dist: python-can>=4.6; extra == 'dev'
47
48
  Requires-Dist: ruff; extra == 'dev'
48
49
  Description-Content-Type: text/markdown
49
50
 
@@ -70,8 +71,8 @@ Use it to:
70
71
  | Format | Reader name | Extensions | Status | Dependency |
71
72
  |---|---|---|---|---|
72
73
  | Kvaser CanKing TXT | `kvaser-txt` | `.txt` | Supported | none |
73
- | candump text | `candump` | `.log` | Planned | none |
74
- | Vector ASC | `vector-asc` | `.asc` | Planned | none |
74
+ | candump text | `candump` | `.log` | Supported | none |
75
+ | Vector ASC | `vector-asc` | `.asc` | Supported | none |
75
76
  | PCAN TRC | `pcan-trc` | `.trc` | Planned | none |
76
77
  | Generic CSV | `csv-table` | `.csv` | Planned | none |
77
78
  | Vector BLF | `vector-blf` | `.blf` | Planned adapter | `python-can` |
@@ -112,7 +113,7 @@ meta = capkit.probe("trace.txt")
112
113
  print(meta.format, meta.start_time)
113
114
 
114
115
  # registered reader names
115
- print(capkit.available_formats()) # ['kvaser-txt']
116
+ print(capkit.available_formats()) # ['candump', 'kvaser-txt', 'vector-asc']
116
117
  ```
117
118
 
118
119
  The public API is six names: `read`, `probe`, `available_formats`,
@@ -165,7 +166,9 @@ class MyReader:
165
166
  capkit.register_reader(MyReader)
166
167
  ```
167
168
 
168
- Registration is process-global and is normally performed at import time.
169
+ Registration is process-global. Installed packages can also advertise reader
170
+ classes through the `capkit.readers` entry-point group; capkit discovers and
171
+ caches them on the first `read()`, `probe()`, or `available_formats()` call.
169
172
  dbckit's `.txt` entry point sniffs among all registered readers, so a reader
170
173
  whose `sniff()` uniquely matches the content of a `.txt` log is used there
171
174
  too, regardless of the extensions it claims.
@@ -200,8 +203,8 @@ for decoded in dbckit.decode_frames(db, capkit.read("trace.txt")):
200
203
  ```
201
204
 
202
205
  capkit also registers its readers in dbckit's `dbckit.readers` entry-point
203
- group. With both packages installed, `dbckit.decode_log()` reads `.txt` logs
204
- through capkit directly:
206
+ group. With both packages installed, `dbckit.decode_log()` reads `.txt`,
207
+ `.log`, and `.asc` logs through capkit directly:
205
208
 
206
209
  ```python
207
210
  for decoded in dbckit.decode_log(db, "trace.txt"):
@@ -212,6 +215,10 @@ for decoded in dbckit.decode_log(db, "trace.txt"):
212
215
 
213
216
  - Kvaser dialects with absolute start-time headers are not supported;
214
217
  `probe()` returns `start_time=None` for `kvaser-txt`.
218
+ - candump error-flag records are skipped by default and rejected in strict
219
+ mode; decoding them as CAN error frames is not claimed.
220
+ - Vector ASC relative timestamp directives and non-English month names are
221
+ rejected instead of being interpreted approximately.
215
222
  - capkit reads frames only: no DBC or signal awareness (that is dbckit's job),
216
223
  no hardware I/O, no log writing, no dataframe export, no CLI.
217
224
 
@@ -21,8 +21,8 @@ Use it to:
21
21
  | Format | Reader name | Extensions | Status | Dependency |
22
22
  |---|---|---|---|---|
23
23
  | Kvaser CanKing TXT | `kvaser-txt` | `.txt` | Supported | none |
24
- | candump text | `candump` | `.log` | Planned | none |
25
- | Vector ASC | `vector-asc` | `.asc` | Planned | none |
24
+ | candump text | `candump` | `.log` | Supported | none |
25
+ | Vector ASC | `vector-asc` | `.asc` | Supported | none |
26
26
  | PCAN TRC | `pcan-trc` | `.trc` | Planned | none |
27
27
  | Generic CSV | `csv-table` | `.csv` | Planned | none |
28
28
  | Vector BLF | `vector-blf` | `.blf` | Planned adapter | `python-can` |
@@ -63,7 +63,7 @@ meta = capkit.probe("trace.txt")
63
63
  print(meta.format, meta.start_time)
64
64
 
65
65
  # registered reader names
66
- print(capkit.available_formats()) # ['kvaser-txt']
66
+ print(capkit.available_formats()) # ['candump', 'kvaser-txt', 'vector-asc']
67
67
  ```
68
68
 
69
69
  The public API is six names: `read`, `probe`, `available_formats`,
@@ -116,7 +116,9 @@ class MyReader:
116
116
  capkit.register_reader(MyReader)
117
117
  ```
118
118
 
119
- Registration is process-global and is normally performed at import time.
119
+ Registration is process-global. Installed packages can also advertise reader
120
+ classes through the `capkit.readers` entry-point group; capkit discovers and
121
+ caches them on the first `read()`, `probe()`, or `available_formats()` call.
120
122
  dbckit's `.txt` entry point sniffs among all registered readers, so a reader
121
123
  whose `sniff()` uniquely matches the content of a `.txt` log is used there
122
124
  too, regardless of the extensions it claims.
@@ -151,8 +153,8 @@ for decoded in dbckit.decode_frames(db, capkit.read("trace.txt")):
151
153
  ```
152
154
 
153
155
  capkit also registers its readers in dbckit's `dbckit.readers` entry-point
154
- group. With both packages installed, `dbckit.decode_log()` reads `.txt` logs
155
- through capkit directly:
156
+ group. With both packages installed, `dbckit.decode_log()` reads `.txt`,
157
+ `.log`, and `.asc` logs through capkit directly:
156
158
 
157
159
  ```python
158
160
  for decoded in dbckit.decode_log(db, "trace.txt"):
@@ -163,6 +165,10 @@ for decoded in dbckit.decode_log(db, "trace.txt"):
163
165
 
164
166
  - Kvaser dialects with absolute start-time headers are not supported;
165
167
  `probe()` returns `start_time=None` for `kvaser-txt`.
168
+ - candump error-flag records are skipped by default and rejected in strict
169
+ mode; decoding them as CAN error frames is not claimed.
170
+ - Vector ASC relative timestamp directives and non-English month names are
171
+ rejected instead of being interpreted approximately.
166
172
  - capkit reads frames only: no DBC or signal awareness (that is dbckit's job),
167
173
  no hardware I/O, no log writing, no dataframe export, no CLI.
168
174
 
@@ -0,0 +1,45 @@
1
+ # Roadmap
2
+
3
+ capkit 0.2.0 is [on PyPI](https://pypi.org/project/capkit/); the release checklist is complete
4
+ and retired (the release process lives in [docs/releasing.md](docs/releasing.md)). Readers are
5
+ fixture-first: a format lands only with a real capture under `tests/fixtures/<format>/`.
6
+
7
+ ## Planned readers
8
+
9
+ - [ ] PCAN TRC (`.trc`)
10
+ - [ ] Configurable generic CSV (`.csv`)
11
+ - [ ] SocketCAN pcap/pcapng (`.pcap`, `.pcapng`)
12
+ - [ ] Vector BLF (`.blf`) and ASAM MF4 (`.mf4`) through optional adapters
13
+ - [ ] Additional Kvaser dialects (absolute start-time headers) as real fixtures arrive
14
+
15
+ ## Reading pipeline
16
+
17
+ - [ ] File-object and stdin input alongside paths, so live captures pipe straight in
18
+ (`candump can0 | ...`)
19
+ - [ ] Transparent reading of gzip-compressed logs (stdlib `gzip`; captures are large)
20
+ - [ ] Collected-error mode between the two current extremes: skip unrecognized lines but
21
+ report them (count and line numbers) instead of skipping silently or raising on the
22
+ first
23
+
24
+ ## Stream operations
25
+
26
+ - [ ] Filters: by ID set, channel, and time window
27
+ - [ ] Merge several logs into one time-ordered stream (multi-bus and multi-file captures)
28
+ - [ ] Explicit, opt-in timestamp rebasing helper — `read()` itself keeps returning
29
+ timestamps exactly as recorded
30
+ - [ ] J1939 arbitration-ID decomposition (priority, PGN, source address): pure per-frame
31
+ arithmetic with no DBC awareness, which is where dbckit's J1939 helpers deliberately
32
+ stop
33
+
34
+ ## CLI (`capkit[cli]`)
35
+
36
+ - [ ] `probe`, `head`, and `stats` commands (unique IDs, per-ID counts, time span)
37
+ - [ ] `convert` between supported formats once writers exist
38
+
39
+ ## Deferred additions
40
+
41
+ - [ ] Writers and format conversion
42
+ - [ ] Dataframe export (pandas/polars) — recipes first, optional helpers only if the
43
+ recipes prove insufficient
44
+ - [ ] python-can `Message` interop helpers
45
+ - [ ] Benchmark suite pinning parse throughput on large logs
@@ -19,6 +19,8 @@ class Frame:
19
19
  is_error_frame: bool = False
20
20
  is_rx: bool | None = None
21
21
  dlc: int | None = None
22
+ bitrate_switch: bool = False
23
+ error_state_indicator: bool = False
22
24
 
23
25
 
24
26
  @dataclass(frozen=True, slots=True)
@@ -1,19 +1,24 @@
1
1
  """Reader registry and format-resolution logic."""
2
2
  from __future__ import annotations
3
3
 
4
+ from importlib import metadata
4
5
  from pathlib import Path
5
6
  from typing import cast
6
7
 
7
8
  from capkit.readers.base import Reader, read_sample
9
+ from capkit.readers.candump import CandumpReader
8
10
  from capkit.readers.kvaser_txt import KvaserTxtReader
11
+ from capkit.readers.vector_asc import VectorAscReader
9
12
 
10
13
  _READERS: dict[str, type[Reader]] = {
14
+ CandumpReader.name: cast(type[Reader], CandumpReader),
11
15
  KvaserTxtReader.name: cast(type[Reader], KvaserTxtReader),
16
+ VectorAscReader.name: cast(type[Reader], VectorAscReader),
12
17
  }
18
+ _ENTRY_POINTS_LOADED = False
13
19
 
14
20
 
15
- def register_reader(reader_type: type[Reader]) -> None:
16
- """Register a zero-argument reader class for process-wide format resolution."""
21
+ def _validate_reader(reader_type: object) -> tuple[str, type[Reader]]:
17
22
  if not isinstance(reader_type, type):
18
23
  raise TypeError("register_reader() requires a reader class, not an instance.")
19
24
 
@@ -22,9 +27,6 @@ def register_reader(reader_type: type[Reader]) -> None:
22
27
  raise TypeError("Reader type must define a non-empty string 'name'.")
23
28
 
24
29
  normalized = name.strip().lower()
25
- if normalized in _READERS:
26
- raise ValueError(f"Log format '{normalized}' is already registered.")
27
-
28
30
  extensions = getattr(reader_type, "extensions", None)
29
31
  if (
30
32
  not isinstance(extensions, tuple)
@@ -38,16 +40,75 @@ def register_reader(reader_type: type[Reader]) -> None:
38
40
  try:
39
41
  reader = reader_type()
40
42
  except Exception as error:
41
- raise TypeError(f"Reader type '{normalized}' must be zero-argument constructible.") from error
43
+ raise TypeError(
44
+ f"Reader type '{normalized}' must be zero-argument constructible: {error}"
45
+ ) from error
42
46
 
43
47
  if any(not callable(getattr(reader, method, None)) for method in ("sniff", "probe", "read")):
44
48
  raise TypeError(f"Reader type '{normalized}' must define callable sniff(), probe(), and read() methods.")
45
49
 
46
- _READERS[normalized] = reader_type
50
+ return normalized, cast(type[Reader], reader_type)
51
+
52
+
53
+ def register_reader(reader_type: type[Reader]) -> None:
54
+ """Register a zero-argument reader class for process-wide format resolution."""
55
+ normalized, validated_type = _validate_reader(reader_type)
56
+ if normalized in _READERS:
57
+ raise ValueError(f"Log format '{normalized}' is already registered.")
58
+ _READERS[normalized] = validated_type
59
+
60
+
61
+ def _distribution_name(entry_point: metadata.EntryPoint) -> str:
62
+ distribution = getattr(entry_point, "dist", None)
63
+ name = getattr(distribution, "name", None)
64
+ if isinstance(name, str) and name:
65
+ return name
66
+ distribution_metadata = getattr(distribution, "metadata", None)
67
+ if distribution_metadata is not None:
68
+ try:
69
+ metadata_name = distribution_metadata["Name"]
70
+ except (KeyError, TypeError):
71
+ pass
72
+ else:
73
+ if isinstance(metadata_name, str) and metadata_name:
74
+ return metadata_name
75
+ return "unknown"
76
+
77
+
78
+ def _entry_point_context(entry_point: metadata.EntryPoint) -> str:
79
+ return (
80
+ f"capkit.readers entry point '{entry_point.name}' "
81
+ f"from distribution '{_distribution_name(entry_point)}'"
82
+ )
83
+
84
+
85
+ def _load_entry_point_readers() -> None:
86
+ global _ENTRY_POINTS_LOADED
87
+ if _ENTRY_POINTS_LOADED:
88
+ return
89
+
90
+ discovered: dict[str, type[Reader]] = {}
91
+ for entry_point in metadata.entry_points(group="capkit.readers"):
92
+ context = _entry_point_context(entry_point)
93
+ try:
94
+ loaded = entry_point.load()
95
+ normalized, reader_type = _validate_reader(loaded)
96
+ except Exception as error:
97
+ raise RuntimeError(f"Failed to load {context}: {error}") from error
98
+
99
+ if normalized in _READERS or normalized in discovered:
100
+ raise RuntimeError(
101
+ f"Reader {context} conflicts with already registered log format '{normalized}'."
102
+ )
103
+ discovered[normalized] = reader_type
104
+
105
+ _READERS.update(discovered)
106
+ _ENTRY_POINTS_LOADED = True
47
107
 
48
108
 
49
109
  def registered_formats() -> list[str]:
50
110
  """Return sorted registered reader names."""
111
+ _load_entry_point_readers()
51
112
  return sorted(_READERS)
52
113
 
53
114
 
@@ -61,6 +122,7 @@ def _reader_type_for(
61
122
  format: str | None = None,
62
123
  sniff_only: bool = False,
63
124
  ) -> type[Reader]:
125
+ _load_entry_point_readers()
64
126
  if not _READERS:
65
127
  raise ValueError("No log formats are registered.")
66
128
 
@@ -101,4 +163,4 @@ def create_reader(
101
163
  return cast(Reader, reader_type(**(options or {})))
102
164
 
103
165
 
104
- __all__ = ["KvaserTxtReader", "Reader", "register_reader"]
166
+ __all__ = ["CandumpReader", "KvaserTxtReader", "Reader", "VectorAscReader", "register_reader"]
@@ -0,0 +1,98 @@
1
+ """Reader for the can-utils ``candump -L`` text format."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from collections.abc import Iterator
6
+ from pathlib import Path
7
+ from re import Match
8
+
9
+ from capkit.model import Frame, LogMeta
10
+ from capkit.readers.base import read_sample
11
+
12
+
13
+ class CandumpReader:
14
+ """Read the can-utils log format emitted by ``candump -L``."""
15
+
16
+ name = "candump"
17
+ extensions = (".log",)
18
+
19
+ _ERROR_FLAG = 0x20000000
20
+ _FRAME_RE = re.compile(
21
+ r"^\s*"
22
+ r"\((?P<timestamp>\d+(?:\.\d+)?)\)"
23
+ r"\s+(?P<interface>\S+)"
24
+ r"\s+(?P<identifier>[0-9A-Fa-f]{3}|[0-9A-Fa-f]{8})"
25
+ r"(?:"
26
+ r"##(?P<fd_flags>[0-9A-Fa-f])(?P<fd_data>(?:[0-9A-Fa-f]{2})*)"
27
+ r"|#(?:[Rr](?P<remote_dlc>[0-9A-Fa-f]?)|(?P<data>(?:[0-9A-Fa-f]{2})*))"
28
+ r")"
29
+ r"(?:\s+(?P<direction>[RrTt]))?"
30
+ r"\s*$"
31
+ )
32
+
33
+ def __init__(self, *, strict: bool = False) -> None:
34
+ self.strict = strict
35
+
36
+ @staticmethod
37
+ def _channel(interface: str) -> int | None:
38
+ match = re.search(r"(\d+)$", interface)
39
+ return int(match.group(1)) if match is not None else None
40
+
41
+ @staticmethod
42
+ def _is_rx(direction: str | None) -> bool | None:
43
+ if direction is None:
44
+ return None
45
+ return direction.upper() == "R"
46
+
47
+ @classmethod
48
+ def _is_error_frame(cls, match: Match[str]) -> bool:
49
+ return bool(int(match.group("identifier"), 16) & cls._ERROR_FLAG)
50
+
51
+ def sniff(self, sample: str) -> bool:
52
+ """Recognize a complete can-utils log record."""
53
+ return any(self._FRAME_RE.fullmatch(line) is not None for line in sample.splitlines())
54
+
55
+ def probe(self, path: Path) -> LogMeta:
56
+ """Return candump metadata after validating a bounded sample."""
57
+ if not self.sniff(read_sample(path)):
58
+ raise ValueError(f"File '{path.name}' is not a candump log.")
59
+ return LogMeta(format=self.name)
60
+
61
+ def _frame(self, match: Match[str]) -> Frame:
62
+ identifier = match.group("identifier")
63
+ fd_flags = match.group("fd_flags")
64
+ remote_dlc = match.group("remote_dlc")
65
+ is_fd = fd_flags is not None
66
+ is_remote = remote_dlc is not None
67
+ flags = int(fd_flags, 16) if fd_flags is not None else 0
68
+ data_hex = match.group("fd_data") if is_fd else match.group("data")
69
+
70
+ return Frame(
71
+ timestamp=float(match.group("timestamp")),
72
+ arbitration_id=int(identifier, 16),
73
+ data=bytes.fromhex(data_hex or ""),
74
+ channel=self._channel(match.group("interface")),
75
+ is_extended_frame=len(identifier) == 8,
76
+ is_fd=is_fd,
77
+ is_remote_frame=is_remote,
78
+ is_rx=self._is_rx(match.group("direction")),
79
+ dlc=int(remote_dlc, 16) if remote_dlc else None,
80
+ bitrate_switch=bool(flags & 0x1),
81
+ error_state_indicator=bool(flags & 0x2),
82
+ )
83
+
84
+ def read(self, path: Path) -> Iterator[Frame]:
85
+ """Lazily parse can-utils frame records from *path*."""
86
+ with path.open(encoding="latin-1") as stream:
87
+ for line_number, raw_line in enumerate(stream, start=1):
88
+ line = raw_line.rstrip("\r\n")
89
+ if not line.strip():
90
+ continue
91
+
92
+ match = self._FRAME_RE.fullmatch(line)
93
+ if match is None or self._is_error_frame(match):
94
+ if self.strict:
95
+ raise ValueError(f"Unrecognized candump line {line_number}: {line!r}.")
96
+ continue
97
+
98
+ yield self._frame(match)