capkit 0.1.0__py3-none-any.whl

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.
capkit/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """Read CAN log files into a common stream of frame objects."""
2
+ from __future__ import annotations
3
+
4
+ from capkit.io import available_formats, probe, read
5
+ from capkit.model import Frame, LogMeta
6
+ from capkit.readers import register_reader
7
+
8
+ __all__ = [
9
+ # model
10
+ "Frame", "LogMeta",
11
+ # io
12
+ "read", "probe", "available_formats", "register_reader",
13
+ ]
capkit/integration.py ADDED
@@ -0,0 +1,22 @@
1
+ """Adapters exposed to dbckit through package entry points."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterator
5
+ from pathlib import Path
6
+
7
+ from capkit.model import Frame
8
+ from capkit.readers import create_reader
9
+
10
+
11
+ class DispatchReader:
12
+ """Sniff and delegate generic file extensions such as ``.txt``."""
13
+
14
+ def __init__(self, *, strict: bool = False, **reader_options: object) -> None:
15
+ self.strict = strict
16
+ self.reader_options = reader_options
17
+
18
+ def read(self, path: Path) -> Iterator[Frame]:
19
+ """Lazily sniff *path* and yield frames from the matching reader."""
20
+ options = {"strict": self.strict, **self.reader_options}
21
+ reader = create_reader(path, options=options, sniff_only=True)
22
+ yield from reader.read(path)
capkit/io.py ADDED
@@ -0,0 +1,38 @@
1
+ """Public file-reading and format-discovery operations."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterator
5
+ from pathlib import Path
6
+
7
+ from capkit.model import Frame, LogMeta
8
+ from capkit.readers import create_reader, registered_formats
9
+
10
+
11
+ def read(
12
+ path: str | Path,
13
+ *,
14
+ format: str | None = None,
15
+ strict: bool = False,
16
+ **reader_options: object,
17
+ ) -> Iterator[Frame]:
18
+ """Lazily read frames from *path* using an explicit or detected format."""
19
+ source = Path(path)
20
+
21
+ def frames() -> Iterator[Frame]:
22
+ options = {"strict": strict, **reader_options}
23
+ reader = create_reader(source, format=format, options=options)
24
+ yield from reader.read(source)
25
+
26
+ return frames()
27
+
28
+
29
+ def probe(path: str | Path, *, format: str | None = None) -> LogMeta:
30
+ """Return cheap, header-derived metadata for *path*."""
31
+ source = Path(path)
32
+ reader = create_reader(source, format=format)
33
+ return reader.probe(source)
34
+
35
+
36
+ def available_formats() -> list[str]:
37
+ """Return the sorted names of registered log formats."""
38
+ return registered_formats()
@@ -0,0 +1,6 @@
1
+ """Core immutable log models."""
2
+ from __future__ import annotations
3
+
4
+ from capkit.model.frame import Frame, LogMeta
5
+
6
+ __all__ = ["Frame", "LogMeta"]
capkit/model/frame.py ADDED
@@ -0,0 +1,30 @@
1
+ """Common frame and log-metadata models."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class Frame:
10
+ """A single CAN frame read from a log file."""
11
+
12
+ timestamp: float
13
+ arbitration_id: int
14
+ data: bytes
15
+ channel: int | None = None
16
+ is_extended_frame: bool = False
17
+ is_fd: bool = False
18
+ is_remote_frame: bool = False
19
+ is_error_frame: bool = False
20
+ is_rx: bool | None = None
21
+ dlc: int | None = None
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class LogMeta:
26
+ """Cheap, header-derived metadata about a log file."""
27
+
28
+ format: str
29
+ start_time: datetime | None = None
30
+ extra: dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,2 @@
1
+ """Reserved namespace for future stream operations."""
2
+ from __future__ import annotations
capkit/py.typed ADDED
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,104 @@
1
+ """Reader registry and format-resolution logic."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ from capkit.readers.base import Reader, read_sample
8
+ from capkit.readers.kvaser_txt import KvaserTxtReader
9
+
10
+ _READERS: dict[str, type[Reader]] = {
11
+ KvaserTxtReader.name: cast(type[Reader], KvaserTxtReader),
12
+ }
13
+
14
+
15
+ def register_reader(reader_type: type[Reader]) -> None:
16
+ """Register a zero-argument reader class for process-wide format resolution."""
17
+ if not isinstance(reader_type, type):
18
+ raise TypeError("register_reader() requires a reader class, not an instance.")
19
+
20
+ name = getattr(reader_type, "name", None)
21
+ if not isinstance(name, str) or not name.strip():
22
+ raise TypeError("Reader type must define a non-empty string 'name'.")
23
+
24
+ normalized = name.strip().lower()
25
+ if normalized in _READERS:
26
+ raise ValueError(f"Log format '{normalized}' is already registered.")
27
+
28
+ extensions = getattr(reader_type, "extensions", None)
29
+ if (
30
+ not isinstance(extensions, tuple)
31
+ or not extensions
32
+ or any(not isinstance(extension, str) or not extension.startswith(".") for extension in extensions)
33
+ ):
34
+ raise TypeError(
35
+ f"Reader type '{normalized}' must define a non-empty tuple of dot-prefixed extensions."
36
+ )
37
+
38
+ try:
39
+ reader = reader_type()
40
+ except Exception as error:
41
+ raise TypeError(f"Reader type '{normalized}' must be zero-argument constructible.") from error
42
+
43
+ if any(not callable(getattr(reader, method, None)) for method in ("sniff", "probe", "read")):
44
+ raise TypeError(f"Reader type '{normalized}' must define callable sniff(), probe(), and read() methods.")
45
+
46
+ _READERS[normalized] = reader_type
47
+
48
+
49
+ def registered_formats() -> list[str]:
50
+ """Return sorted registered reader names."""
51
+ return sorted(_READERS)
52
+
53
+
54
+ def _available_label() -> str:
55
+ return ", ".join(registered_formats())
56
+
57
+
58
+ def _reader_type_for(
59
+ path: Path,
60
+ *,
61
+ format: str | None = None,
62
+ sniff_only: bool = False,
63
+ ) -> type[Reader]:
64
+ if not _READERS:
65
+ raise ValueError("No log formats are registered.")
66
+
67
+ if format is not None:
68
+ normalized = format.strip().lower()
69
+ reader_type = _READERS.get(normalized)
70
+ if reader_type is None:
71
+ raise ValueError(f"Unknown log format '{format}'. Available formats: {_available_label()}.")
72
+ return reader_type
73
+
74
+ if not sniff_only:
75
+ extension = path.suffix.lower()
76
+ candidates = [
77
+ reader_type
78
+ for reader_type in _READERS.values()
79
+ if extension and extension in (claimed.lower() for claimed in reader_type.extensions)
80
+ ]
81
+ if len(candidates) == 1:
82
+ return candidates[0]
83
+
84
+ sample = read_sample(path)
85
+ matches = [reader_type for reader_type in _READERS.values() if reader_type().sniff(sample)]
86
+ if len(matches) == 1:
87
+ return matches[0]
88
+
89
+ raise ValueError(f"Unknown log format for '{path.name}'. Available formats: {_available_label()}.")
90
+
91
+
92
+ def create_reader(
93
+ path: Path,
94
+ *,
95
+ format: str | None = None,
96
+ options: dict[str, object] | None = None,
97
+ sniff_only: bool = False,
98
+ ) -> Reader:
99
+ """Resolve and construct a reader for *path*."""
100
+ reader_type = _reader_type_for(path, format=format, sniff_only=sniff_only)
101
+ return cast(Reader, reader_type(**(options or {})))
102
+
103
+
104
+ __all__ = ["KvaserTxtReader", "Reader", "register_reader"]
capkit/readers/base.py ADDED
@@ -0,0 +1,35 @@
1
+ """Shared reader protocol and bounded file-sampling helper."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterator
5
+ from pathlib import Path
6
+ from typing import Protocol
7
+
8
+ from capkit.model import Frame, LogMeta
9
+
10
+ SNIFF_SIZE = 4096
11
+
12
+
13
+ class Reader(Protocol):
14
+ """A stateless log-format reader."""
15
+
16
+ name: str
17
+ extensions: tuple[str, ...]
18
+
19
+ def sniff(self, sample: str) -> bool:
20
+ """Return whether *sample* looks like this reader's format."""
21
+ ...
22
+
23
+ def probe(self, path: Path) -> LogMeta:
24
+ """Return cheap metadata from *path*."""
25
+ ...
26
+
27
+ def read(self, path: Path) -> Iterator[Frame]:
28
+ """Lazily yield frames from *path*."""
29
+ ...
30
+
31
+
32
+ def read_sample(path: Path) -> str:
33
+ """Read and Latin-1 decode at most the first 4 KiB of *path*."""
34
+ with path.open("rb") as stream:
35
+ return stream.read(SNIFF_SIZE).decode("latin-1")
@@ -0,0 +1,92 @@
1
+ """Reader for Kvaser CanKing text exports."""
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 KvaserTxtReader:
14
+ """Read the whitespace-delimited Kvaser CanKing TXT dialect."""
15
+
16
+ name = "kvaser-txt"
17
+ extensions = (".txt",)
18
+
19
+ _FRAME_RE = re.compile(
20
+ r"^\s*"
21
+ r"(?P<channel>\d+)"
22
+ r"\s+(?P<identifier>[0-9A-Fa-f]+)"
23
+ r"(?:\s+(?P<flag>[xX]))?"
24
+ r"\s+(?P<dlc>\d+)"
25
+ r"(?P<data>(?:\s+[0-9A-Fa-f]{2})*)"
26
+ r"\s+(?P<timestamp>\d+(?:\.\d+)?)"
27
+ r"\s+(?P<direction>[RrTt])"
28
+ r"\s*$"
29
+ )
30
+
31
+ def __init__(self, *, strict: bool = False) -> None:
32
+ self.strict = strict
33
+
34
+ @staticmethod
35
+ def _is_header(line: str) -> bool:
36
+ normalized = " ".join(line.lower().split())
37
+ return normalized.startswith("chn identifier flg dlc") and "time" in normalized and "dir" in normalized
38
+
39
+ @staticmethod
40
+ def _data_tokens(match: Match[str]) -> list[str]:
41
+ return match.group("data").split()
42
+
43
+ @classmethod
44
+ def _is_complete_frame(cls, match: Match[str]) -> bool:
45
+ return len(cls._data_tokens(match)) == int(match.group("dlc"))
46
+
47
+ def sniff(self, sample: str) -> bool:
48
+ """Recognize the Kvaser table header or a complete frame line."""
49
+ for line in sample.splitlines():
50
+ if self._is_header(line):
51
+ return True
52
+ match = self._FRAME_RE.fullmatch(line)
53
+ if match is not None and self._is_complete_frame(match):
54
+ return True
55
+ return False
56
+
57
+ def probe(self, path: Path) -> LogMeta:
58
+ """Return Kvaser metadata without scanning the frame body."""
59
+ if not self.sniff(read_sample(path)):
60
+ raise ValueError(f"File '{path.name}' is not a Kvaser TXT log.")
61
+ return LogMeta(format=self.name)
62
+
63
+ def read(self, path: Path) -> Iterator[Frame]:
64
+ """Lazily parse Kvaser frame records from *path*."""
65
+ with path.open(encoding="latin-1") as stream:
66
+ for line_number, raw_line in enumerate(stream, start=1):
67
+ line = raw_line.rstrip("\r\n")
68
+ if not line.strip():
69
+ continue
70
+
71
+ match = self._FRAME_RE.fullmatch(line)
72
+ if match is None:
73
+ if self.strict:
74
+ raise ValueError(f"Unrecognized Kvaser TXT line {line_number}: {line!r}.")
75
+ continue
76
+
77
+ data_tokens = self._data_tokens(match)
78
+ dlc = int(match.group("dlc"))
79
+ if len(data_tokens) != dlc:
80
+ raise ValueError(
81
+ f"Invalid Kvaser TXT frame at line {line_number}: "
82
+ f"DLC {dlc} declares {dlc} data bytes, got {len(data_tokens)}."
83
+ )
84
+
85
+ yield Frame(
86
+ timestamp=float(match.group("timestamp")),
87
+ arbitration_id=int(match.group("identifier"), 16),
88
+ data=bytes.fromhex(" ".join(data_tokens)),
89
+ channel=int(match.group("channel")),
90
+ is_extended_frame=match.group("flag") is not None,
91
+ is_rx=match.group("direction").upper() == "R",
92
+ )
@@ -0,0 +1,2 @@
1
+ """Reserved namespace for future log writers."""
2
+ from __future__ import annotations
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: capkit
3
+ Version: 0.1.0
4
+ Summary: Read CAN bus capture logs in different formats into one common frame stream
5
+ Project-URL: Homepage, https://canforge.io/capkit
6
+ Project-URL: Repository, https://github.com/canforge/capkit
7
+ Project-URL: Changelog, https://github.com/canforge/capkit/blob/main/CHANGELOG.md
8
+ Author-email: André Delgado <andre@adelgado.io>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 André Delgado
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: CAN,automotive,canbus,kvaser,log
32
+ Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Classifier: Programming Language :: Python :: 3.14
40
+ Requires-Python: >=3.11
41
+ Provides-Extra: dev
42
+ Requires-Dist: build; extra == 'dev'
43
+ Requires-Dist: dbckit>=1.0; extra == 'dev'
44
+ Requires-Dist: mypy; extra == 'dev'
45
+ Requires-Dist: pytest-cov; extra == 'dev'
46
+ Requires-Dist: pytest>=8.0; extra == 'dev'
47
+ Requires-Dist: ruff; extra == 'dev'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # capkit
51
+
52
+ [![PyPI](https://img.shields.io/pypi/v/capkit)](https://pypi.org/project/capkit/)
53
+ [![CI](https://github.com/canforge/capkit/actions/workflows/ci.yml/badge.svg)](https://github.com/canforge/capkit/actions/workflows/ci.yml)
54
+ [![Python versions](https://img.shields.io/pypi/pyversions/capkit)](https://pypi.org/project/capkit/)
55
+ [![License: MIT](https://img.shields.io/pypi/l/capkit)](LICENSE)
56
+
57
+ `capkit` is a Python library that reads **CAN bus capture logs into one common
58
+ frame stream**. Every supported format parses into the same frozen `Frame`
59
+ dataclass, so code that consumes frames never depends on which tool captured
60
+ the log.
61
+
62
+ Use it to:
63
+
64
+ - read captures from different tools as one lazy stream of typed `Frame` objects
65
+ - probe a file for header metadata without scanning the frame body
66
+ - detect the log format from the file extension or the file content
67
+ - skip real-world log noise by default, or reject it with `strict=True`
68
+ - feed frames into [dbckit](https://github.com/canforge/dbckit) for DBC signal decoding
69
+
70
+ | Format | Reader name | Extensions | Status | Dependency |
71
+ |---|---|---|---|---|
72
+ | Kvaser CanKing TXT | `kvaser-txt` | `.txt` | Supported | none |
73
+ | candump text | `candump` | `.log` | Planned | none |
74
+ | Vector ASC | `vector-asc` | `.asc` | Planned | none |
75
+ | PCAN TRC | `pcan-trc` | `.trc` | Planned | none |
76
+ | Generic CSV | `csv-table` | `.csv` | Planned | none |
77
+ | Vector BLF | `vector-blf` | `.blf` | Planned adapter | `python-can` |
78
+ | ASAM MF4 | `asam-mf4` | `.mf4` | Planned adapter | `asammdf` |
79
+
80
+ See [format support](docs/format-support.md) for the exact dialect each reader
81
+ accepts, and the [roadmap](ROADMAP.md) for sequencing.
82
+
83
+ ## Install
84
+
85
+ ```bash
86
+ pip install capkit
87
+ ```
88
+
89
+ Requires Python `>=3.11`. capkit has no runtime dependencies.
90
+
91
+ ## Design
92
+
93
+ - `Frame` and `LogMeta` are frozen, slotted dataclasses.
94
+ - `read()` is lazy and keeps constant parser state, so file size does not matter.
95
+ - Timestamps are returned exactly as recorded in the source, never rebased or
96
+ converted to absolute time.
97
+ - A format is added only when a real captured fixture pins its dialect under
98
+ `tests/fixtures/`; unsupported dialects fail clearly instead of parsing
99
+ approximately.
100
+
101
+ ## Quick Start
102
+
103
+ ```python
104
+ import capkit
105
+
106
+ # stream frames
107
+ for frame in capkit.read("trace.txt"):
108
+ print(frame.timestamp, hex(frame.arbitration_id), frame.data.hex())
109
+
110
+ # header metadata only
111
+ meta = capkit.probe("trace.txt")
112
+ print(meta.format, meta.start_time)
113
+
114
+ # registered reader names
115
+ print(capkit.available_formats()) # ['kvaser-txt']
116
+ ```
117
+
118
+ The public API is six names: `read`, `probe`, `available_formats`,
119
+ `register_reader`, `Frame`, and `LogMeta`.
120
+
121
+ ## Features
122
+
123
+ ### Format detection
124
+
125
+ An explicit `format=` names a reader and takes precedence over the file
126
+ extension:
127
+
128
+ ```python
129
+ frames = capkit.read("capture.bin", format="kvaser-txt")
130
+ ```
131
+
132
+ Without `format=`, capkit matches the extension against registered readers and
133
+ sniffs the first 4 KiB when the extension is unknown or ambiguous.
134
+
135
+ ### Add your own reader
136
+
137
+ Register a zero-argument reader class to make it available to `read()`,
138
+ `probe()`, and format detection:
139
+
140
+ ```python
141
+ from collections.abc import Iterator
142
+ from pathlib import Path
143
+
144
+ import capkit
145
+
146
+
147
+ class MyReader:
148
+ name: str = "my-format"
149
+ extensions: tuple[str, ...] = (".mylog",)
150
+
151
+ def __init__(self, *, strict: bool = False) -> None:
152
+ self.strict = strict
153
+
154
+ def sniff(self, sample: str) -> bool:
155
+ return sample.startswith("MYLOG")
156
+
157
+ def probe(self, path: Path) -> capkit.LogMeta:
158
+ return capkit.LogMeta(format=self.name)
159
+
160
+ def read(self, path: Path) -> Iterator[capkit.Frame]:
161
+ # Parse path lazily and yield capkit.Frame objects here.
162
+ yield from ()
163
+
164
+
165
+ capkit.register_reader(MyReader)
166
+ ```
167
+
168
+ Registration is process-global and is normally performed at import time.
169
+ dbckit's `.txt` entry point sniffs among all registered readers, so a reader
170
+ whose `sniff()` uniquely matches the content of a `.txt` log is used there
171
+ too, regardless of the extensions it claims.
172
+
173
+ ### Dirty logs and strict mode
174
+
175
+ Readers skip headers, trailers, comments, blank lines, and unrelated noise by
176
+ default. Pass `strict=True` to raise a line-numbered `ValueError` on the first
177
+ unrecognized nonblank line instead:
178
+
179
+ ```python
180
+ frames = capkit.read("trace.txt", strict=True)
181
+ ```
182
+
183
+ A frame record whose DLC disagrees with its data bytes raises in both modes;
184
+ corrupt frames are never silently dropped.
185
+
186
+ ## Use with dbckit
187
+
188
+ [dbckit](https://github.com/canforge/dbckit) decodes CAN frames against a DBC
189
+ database. capkit and dbckit are separate packages — neither depends on or
190
+ imports the other — with adjacent jobs: capkit turns bytes on disk into frames,
191
+ dbckit turns frames plus a DBC into signals.
192
+
193
+ ```python
194
+ import capkit
195
+ import dbckit
196
+
197
+ db = dbckit.load("truck.dbc")
198
+ for decoded in dbckit.decode_frames(db, capkit.read("trace.txt")):
199
+ print(decoded.timestamp, decoded.signals)
200
+ ```
201
+
202
+ 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:
205
+
206
+ ```python
207
+ for decoded in dbckit.decode_log(db, "trace.txt"):
208
+ print(decoded.signals)
209
+ ```
210
+
211
+ ## Scope and Caveats
212
+
213
+ - Kvaser dialects with absolute start-time headers are not supported;
214
+ `probe()` returns `start_time=None` for `kvaser-txt`.
215
+ - capkit reads frames only: no DBC or signal awareness (that is dbckit's job),
216
+ no hardware I/O, no log writing, no dataframe export, no CLI.
217
+
218
+ ## Documentation
219
+
220
+ - [Format support](docs/format-support.md) — supported formats and the exact
221
+ dialect each reader accepts
222
+ - [API reference](docs/api-reference.md) — the public API contract
223
+ - [Recipes](docs/recipes.md) — counting IDs, filtering, time windows, CSV
224
+ export, and dataframes in a few lines of standard library
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ python -m venv .venv
230
+ source .venv/bin/activate
231
+ pip install -e ".[dev]"
232
+ pytest
233
+ ```
234
+
235
+ The `dev` extra includes dbckit so the entry-point integration tests run; the
236
+ core and contract suites pass without it.
237
+
238
+ ## License
239
+
240
+ MIT
@@ -0,0 +1,16 @@
1
+ capkit/__init__.py,sha256=l6KOaVUfBHbeibn7CX1xACeT-uxq9BEfSzPIQQR-LGU,357
2
+ capkit/integration.py,sha256=Yc7BNz-eByJh9wMQ08SkgAgwzhux5ZLCyDqxo3Zmno8,789
3
+ capkit/io.py,sha256=2gLk-zCjlW9zpmZLL1X4y4jttJh0npkjajOhROoavS4,1107
4
+ capkit/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
5
+ capkit/model/__init__.py,sha256=Rg3bkQjSPsGIsXoZAfMiSeeeiPO6S6jvMoFr_InTxQw,147
6
+ capkit/model/frame.py,sha256=XQyqVHuSAh_fI3zjLZJEkqaFauj8cb8tBvdpbwYdzSk,754
7
+ capkit/operations/__init__.py,sha256=ksgo7TaQtvU9fvGGrrfAyYS4o-axtDbkPGGZ9I_dwfs,90
8
+ capkit/readers/__init__.py,sha256=3BQ9ZT8-xwRNNO3fNZ-GYijlnjg1q69Qbp9Ma7bC4Ro,3475
9
+ capkit/readers/base.py,sha256=YZD1kKgRP6l06u2zhAvYSaRD-Y3E3emjyTv6nBBi74Q,906
10
+ capkit/readers/kvaser_txt.py,sha256=PmOsHW4mikQ6T9ZOTS--nHut7VxEpszmRpg2u2w84KA,3386
11
+ capkit/writers/__init__.py,sha256=9XwmWR0Y4bp3KnIh0MMjQk1cHZjX_bX6-su3Ho8hWzw,84
12
+ capkit-0.1.0.dist-info/METADATA,sha256=6UUFM4aAKMm7XLO_svalqC44L3s5wmEF37XNQobFtRA,8470
13
+ capkit-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
14
+ capkit-0.1.0.dist-info/entry_points.txt,sha256=5NWQwqFV5V6S-qnkTgr2o8lFoLu87Z9HN36gevqDNmQ,57
15
+ capkit-0.1.0.dist-info/licenses/LICENSE,sha256=VArw2mCCiRiU3jUc906yLPYKJCOtrNFyAINYPHzO-1g,1071
16
+ capkit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [dbckit.readers]
2
+ txt = capkit.integration:DispatchReader
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 André Delgado
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.