capkit 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.
- capkit-0.1.0/.github/workflows/ci.yml +63 -0
- capkit-0.1.0/.github/workflows/release.yml +38 -0
- capkit-0.1.0/.gitignore +29 -0
- capkit-0.1.0/CHANGELOG.md +27 -0
- capkit-0.1.0/LICENSE +21 -0
- capkit-0.1.0/PKG-INFO +240 -0
- capkit-0.1.0/README.md +191 -0
- capkit-0.1.0/ROADMAP.md +71 -0
- capkit-0.1.0/capkit/__init__.py +13 -0
- capkit-0.1.0/capkit/integration.py +22 -0
- capkit-0.1.0/capkit/io.py +38 -0
- capkit-0.1.0/capkit/model/__init__.py +6 -0
- capkit-0.1.0/capkit/model/frame.py +30 -0
- capkit-0.1.0/capkit/operations/__init__.py +2 -0
- capkit-0.1.0/capkit/py.typed +1 -0
- capkit-0.1.0/capkit/readers/__init__.py +104 -0
- capkit-0.1.0/capkit/readers/base.py +35 -0
- capkit-0.1.0/capkit/readers/kvaser_txt.py +92 -0
- capkit-0.1.0/capkit/writers/__init__.py +2 -0
- capkit-0.1.0/docs/api-reference.md +147 -0
- capkit-0.1.0/docs/format-support.md +54 -0
- capkit-0.1.0/docs/recipes.md +91 -0
- capkit-0.1.0/docs/releasing.md +84 -0
- capkit-0.1.0/pyproject.toml +74 -0
- capkit-0.1.0/tests/__init__.py +0 -0
- capkit-0.1.0/tests/fixtures/kvaser/kvaser.txt +302 -0
- capkit-0.1.0/tests/test_contract.py +39 -0
- capkit-0.1.0/tests/test_integration_dbckit.py +52 -0
- capkit-0.1.0/tests/test_io.py +265 -0
- capkit-0.1.0/tests/test_kvaser_txt.py +112 -0
- capkit-0.1.0/tests/test_model.py +40 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main", "master"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["main", "master"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: "Test (Python ${{ matrix.python-version }})"
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: python -m pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: ruff check .
|
|
31
|
+
|
|
32
|
+
- name: Type-check
|
|
33
|
+
run: mypy capkit
|
|
34
|
+
|
|
35
|
+
- name: Test and coverage gate
|
|
36
|
+
run: python -m pytest --cov=capkit --cov-fail-under=90 -q
|
|
37
|
+
|
|
38
|
+
build:
|
|
39
|
+
name: Build check
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
needs: test
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
- uses: actions/setup-python@v5
|
|
46
|
+
with:
|
|
47
|
+
python-version: "3.11"
|
|
48
|
+
- name: Install build frontend
|
|
49
|
+
run: python -m pip install build
|
|
50
|
+
- name: Build sdist and wheel
|
|
51
|
+
run: python -m build
|
|
52
|
+
- name: Verify wheel metadata
|
|
53
|
+
run: >-
|
|
54
|
+
python -c "from pathlib import Path;
|
|
55
|
+
import zipfile;
|
|
56
|
+
wheel=next(Path('dist').glob('*.whl'));
|
|
57
|
+
archive=zipfile.ZipFile(wheel);
|
|
58
|
+
names=archive.namelist();
|
|
59
|
+
assert 'capkit/py.typed' in names;
|
|
60
|
+
entry=next(name for name in names if name.endswith('entry_points.txt'));
|
|
61
|
+
text=archive.read(entry).decode();
|
|
62
|
+
assert '[dbckit.readers]' in text;
|
|
63
|
+
assert 'txt = capkit.integration:DispatchReader' in text"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: "Build distributions"
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- name: Install build
|
|
17
|
+
run: pip install build
|
|
18
|
+
- name: Build sdist and wheel
|
|
19
|
+
run: python -m build
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
name: "Publish to PyPI"
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
needs: build
|
|
29
|
+
environment: pypi
|
|
30
|
+
permissions:
|
|
31
|
+
id-token: write
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@v4
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
- name: Publish (trusted publishing / OIDC)
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
capkit-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
|
|
3
|
+
.claude/
|
|
4
|
+
.venv/
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[cod]
|
|
7
|
+
|
|
8
|
+
.env
|
|
9
|
+
.env.*
|
|
10
|
+
!.env.example
|
|
11
|
+
|
|
12
|
+
.tox/
|
|
13
|
+
.nox/
|
|
14
|
+
.hypothesis/
|
|
15
|
+
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
coverage.xml
|
|
19
|
+
junit.xml
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
.mypy_cache/
|
|
23
|
+
.ruff_cache/
|
|
24
|
+
|
|
25
|
+
dist/
|
|
26
|
+
build/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
|
|
29
|
+
DEFINITION.md
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to capkit are documented here.
|
|
4
|
+
|
|
5
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
Versioning will follow [Semantic Versioning](https://semver.org/) from 1.0.0 onward.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.1.0] — 2026-07-16
|
|
13
|
+
|
|
14
|
+
Initial public release.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Frozen, slotted `Frame` and `LogMeta` models; no runtime dependencies.
|
|
19
|
+
- Lazy `read()`, header-only `probe()`, and `available_formats()`.
|
|
20
|
+
- Public `register_reader()` for validated, process-wide custom reader classes.
|
|
21
|
+
- Kvaser CanKing TXT reader (`kvaser-txt`) with noise-skipping and strict modes,
|
|
22
|
+
pinned by an anonymized excerpt of a real capture.
|
|
23
|
+
- Generic `.txt` dispatch through the `dbckit.readers` entry-point group.
|
|
24
|
+
- `py.typed` typing metadata.
|
|
25
|
+
- CI on Python 3.11–3.14: pytest with coverage ≥ 90%, ruff, mypy, and a wheel
|
|
26
|
+
metadata build check.
|
|
27
|
+
- Tag-driven release workflow publishing to PyPI with trusted publishing (OIDC).
|
capkit-0.1.0/LICENSE
ADDED
|
@@ -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.
|
capkit-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/capkit/)
|
|
53
|
+
[](https://github.com/canforge/capkit/actions/workflows/ci.yml)
|
|
54
|
+
[](https://pypi.org/project/capkit/)
|
|
55
|
+
[](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
|
capkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# capkit
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/capkit/)
|
|
4
|
+
[](https://github.com/canforge/capkit/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pypi.org/project/capkit/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
`capkit` is a Python library that reads **CAN bus capture logs into one common
|
|
9
|
+
frame stream**. Every supported format parses into the same frozen `Frame`
|
|
10
|
+
dataclass, so code that consumes frames never depends on which tool captured
|
|
11
|
+
the log.
|
|
12
|
+
|
|
13
|
+
Use it to:
|
|
14
|
+
|
|
15
|
+
- read captures from different tools as one lazy stream of typed `Frame` objects
|
|
16
|
+
- probe a file for header metadata without scanning the frame body
|
|
17
|
+
- detect the log format from the file extension or the file content
|
|
18
|
+
- skip real-world log noise by default, or reject it with `strict=True`
|
|
19
|
+
- feed frames into [dbckit](https://github.com/canforge/dbckit) for DBC signal decoding
|
|
20
|
+
|
|
21
|
+
| Format | Reader name | Extensions | Status | Dependency |
|
|
22
|
+
|---|---|---|---|---|
|
|
23
|
+
| Kvaser CanKing TXT | `kvaser-txt` | `.txt` | Supported | none |
|
|
24
|
+
| candump text | `candump` | `.log` | Planned | none |
|
|
25
|
+
| Vector ASC | `vector-asc` | `.asc` | Planned | none |
|
|
26
|
+
| PCAN TRC | `pcan-trc` | `.trc` | Planned | none |
|
|
27
|
+
| Generic CSV | `csv-table` | `.csv` | Planned | none |
|
|
28
|
+
| Vector BLF | `vector-blf` | `.blf` | Planned adapter | `python-can` |
|
|
29
|
+
| ASAM MF4 | `asam-mf4` | `.mf4` | Planned adapter | `asammdf` |
|
|
30
|
+
|
|
31
|
+
See [format support](docs/format-support.md) for the exact dialect each reader
|
|
32
|
+
accepts, and the [roadmap](ROADMAP.md) for sequencing.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install capkit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python `>=3.11`. capkit has no runtime dependencies.
|
|
41
|
+
|
|
42
|
+
## Design
|
|
43
|
+
|
|
44
|
+
- `Frame` and `LogMeta` are frozen, slotted dataclasses.
|
|
45
|
+
- `read()` is lazy and keeps constant parser state, so file size does not matter.
|
|
46
|
+
- Timestamps are returned exactly as recorded in the source, never rebased or
|
|
47
|
+
converted to absolute time.
|
|
48
|
+
- A format is added only when a real captured fixture pins its dialect under
|
|
49
|
+
`tests/fixtures/`; unsupported dialects fail clearly instead of parsing
|
|
50
|
+
approximately.
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import capkit
|
|
56
|
+
|
|
57
|
+
# stream frames
|
|
58
|
+
for frame in capkit.read("trace.txt"):
|
|
59
|
+
print(frame.timestamp, hex(frame.arbitration_id), frame.data.hex())
|
|
60
|
+
|
|
61
|
+
# header metadata only
|
|
62
|
+
meta = capkit.probe("trace.txt")
|
|
63
|
+
print(meta.format, meta.start_time)
|
|
64
|
+
|
|
65
|
+
# registered reader names
|
|
66
|
+
print(capkit.available_formats()) # ['kvaser-txt']
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The public API is six names: `read`, `probe`, `available_formats`,
|
|
70
|
+
`register_reader`, `Frame`, and `LogMeta`.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
### Format detection
|
|
75
|
+
|
|
76
|
+
An explicit `format=` names a reader and takes precedence over the file
|
|
77
|
+
extension:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
frames = capkit.read("capture.bin", format="kvaser-txt")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Without `format=`, capkit matches the extension against registered readers and
|
|
84
|
+
sniffs the first 4 KiB when the extension is unknown or ambiguous.
|
|
85
|
+
|
|
86
|
+
### Add your own reader
|
|
87
|
+
|
|
88
|
+
Register a zero-argument reader class to make it available to `read()`,
|
|
89
|
+
`probe()`, and format detection:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from collections.abc import Iterator
|
|
93
|
+
from pathlib import Path
|
|
94
|
+
|
|
95
|
+
import capkit
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MyReader:
|
|
99
|
+
name: str = "my-format"
|
|
100
|
+
extensions: tuple[str, ...] = (".mylog",)
|
|
101
|
+
|
|
102
|
+
def __init__(self, *, strict: bool = False) -> None:
|
|
103
|
+
self.strict = strict
|
|
104
|
+
|
|
105
|
+
def sniff(self, sample: str) -> bool:
|
|
106
|
+
return sample.startswith("MYLOG")
|
|
107
|
+
|
|
108
|
+
def probe(self, path: Path) -> capkit.LogMeta:
|
|
109
|
+
return capkit.LogMeta(format=self.name)
|
|
110
|
+
|
|
111
|
+
def read(self, path: Path) -> Iterator[capkit.Frame]:
|
|
112
|
+
# Parse path lazily and yield capkit.Frame objects here.
|
|
113
|
+
yield from ()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
capkit.register_reader(MyReader)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Registration is process-global and is normally performed at import time.
|
|
120
|
+
dbckit's `.txt` entry point sniffs among all registered readers, so a reader
|
|
121
|
+
whose `sniff()` uniquely matches the content of a `.txt` log is used there
|
|
122
|
+
too, regardless of the extensions it claims.
|
|
123
|
+
|
|
124
|
+
### Dirty logs and strict mode
|
|
125
|
+
|
|
126
|
+
Readers skip headers, trailers, comments, blank lines, and unrelated noise by
|
|
127
|
+
default. Pass `strict=True` to raise a line-numbered `ValueError` on the first
|
|
128
|
+
unrecognized nonblank line instead:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
frames = capkit.read("trace.txt", strict=True)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A frame record whose DLC disagrees with its data bytes raises in both modes;
|
|
135
|
+
corrupt frames are never silently dropped.
|
|
136
|
+
|
|
137
|
+
## Use with dbckit
|
|
138
|
+
|
|
139
|
+
[dbckit](https://github.com/canforge/dbckit) decodes CAN frames against a DBC
|
|
140
|
+
database. capkit and dbckit are separate packages — neither depends on or
|
|
141
|
+
imports the other — with adjacent jobs: capkit turns bytes on disk into frames,
|
|
142
|
+
dbckit turns frames plus a DBC into signals.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import capkit
|
|
146
|
+
import dbckit
|
|
147
|
+
|
|
148
|
+
db = dbckit.load("truck.dbc")
|
|
149
|
+
for decoded in dbckit.decode_frames(db, capkit.read("trace.txt")):
|
|
150
|
+
print(decoded.timestamp, decoded.signals)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
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
|
+
|
|
157
|
+
```python
|
|
158
|
+
for decoded in dbckit.decode_log(db, "trace.txt"):
|
|
159
|
+
print(decoded.signals)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Scope and Caveats
|
|
163
|
+
|
|
164
|
+
- Kvaser dialects with absolute start-time headers are not supported;
|
|
165
|
+
`probe()` returns `start_time=None` for `kvaser-txt`.
|
|
166
|
+
- capkit reads frames only: no DBC or signal awareness (that is dbckit's job),
|
|
167
|
+
no hardware I/O, no log writing, no dataframe export, no CLI.
|
|
168
|
+
|
|
169
|
+
## Documentation
|
|
170
|
+
|
|
171
|
+
- [Format support](docs/format-support.md) — supported formats and the exact
|
|
172
|
+
dialect each reader accepts
|
|
173
|
+
- [API reference](docs/api-reference.md) — the public API contract
|
|
174
|
+
- [Recipes](docs/recipes.md) — counting IDs, filtering, time windows, CSV
|
|
175
|
+
export, and dataframes in a few lines of standard library
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
python -m venv .venv
|
|
181
|
+
source .venv/bin/activate
|
|
182
|
+
pip install -e ".[dev]"
|
|
183
|
+
pytest
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The `dev` extra includes dbckit so the entry-point integration tests run; the
|
|
187
|
+
core and contract suites pass without it.
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT
|
capkit-0.1.0/ROADMAP.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
Readers are fixture-first: a format lands only with a real capture under
|
|
4
|
+
`tests/fixtures/<format>/`.
|
|
5
|
+
|
|
6
|
+
## Publish 0.1.0 to PyPI
|
|
7
|
+
|
|
8
|
+
- [x] **1. Add `.github/workflows/release.yml`**: trigger on `v*` tags; build sdist + wheel;
|
|
9
|
+
publish with `pypa/gh-action-pypi-publish` using OIDC (`permissions: id-token: write`,
|
|
10
|
+
environment `pypi`) — no API tokens
|
|
11
|
+
- [ ] **2. Register the trusted publisher on pypi.org** *(manual)*: add a *pending publisher*
|
|
12
|
+
for `canforge/capkit`, workflow `release.yml`, environment `pypi`, and create the protected
|
|
13
|
+
`pypi` environment in the GitHub repository settings
|
|
14
|
+
- [ ] **3. Optional TestPyPI dry run**: point a copy of the workflow at TestPyPI, publish,
|
|
15
|
+
`pip install -i https://test.pypi.org/simple/ capkit`
|
|
16
|
+
- [x] **4. Date the `0.1.0` changelog section** and match the `pyproject.toml` version
|
|
17
|
+
- [x] **5. Merge to `main` and push**
|
|
18
|
+
- [ ] **6. Tag and release**: `git tag -a v0.1.0 -m "Release 0.1.0" && git push origin v0.1.0` —
|
|
19
|
+
the workflow builds and publishes
|
|
20
|
+
- [ ] **7. Verify the release**
|
|
21
|
+
- [ ] In a clean venv: `pip install capkit` then
|
|
22
|
+
`python -c "import capkit; print(capkit.available_formats())"`
|
|
23
|
+
- [ ] With dbckit installed alongside, `dbckit.decode_log(db, "trace.txt")` resolves
|
|
24
|
+
capkit's `.txt` entry point
|
|
25
|
+
- [ ] PyPI page shows license, links, and README correctly
|
|
26
|
+
|
|
27
|
+
## Planned readers
|
|
28
|
+
|
|
29
|
+
- [ ] candump text (`.log`)
|
|
30
|
+
- [ ] Vector ASC (`.asc`)
|
|
31
|
+
- [ ] PCAN TRC (`.trc`)
|
|
32
|
+
- [ ] Configurable generic CSV (`.csv`)
|
|
33
|
+
- [ ] SocketCAN pcap/pcapng (`.pcap`, `.pcapng`)
|
|
34
|
+
- [ ] Vector BLF (`.blf`) and ASAM MF4 (`.mf4`) through optional adapters
|
|
35
|
+
- [ ] Additional Kvaser dialects (absolute start-time headers) as real fixtures arrive
|
|
36
|
+
|
|
37
|
+
## Reading pipeline
|
|
38
|
+
|
|
39
|
+
- [ ] A `capkit.readers` entry-point group, so installed third-party formats are
|
|
40
|
+
discovered automatically without import-time registration
|
|
41
|
+
- [ ] File-object and stdin input alongside paths, so live captures pipe straight in
|
|
42
|
+
(`candump can0 | ...`)
|
|
43
|
+
- [ ] Transparent reading of gzip-compressed logs (stdlib `gzip`; captures are large)
|
|
44
|
+
- [ ] Collected-error mode between the two current extremes: skip unrecognized lines but
|
|
45
|
+
report them (count and line numbers) instead of skipping silently or raising on the
|
|
46
|
+
first
|
|
47
|
+
- [ ] CAN FD flags on `Frame` (bit-rate switch, error-state indicator) when the first FD
|
|
48
|
+
fixture lands
|
|
49
|
+
|
|
50
|
+
## Stream operations
|
|
51
|
+
|
|
52
|
+
- [ ] Filters: by ID set, channel, and time window
|
|
53
|
+
- [ ] Merge several logs into one time-ordered stream (multi-bus and multi-file captures)
|
|
54
|
+
- [ ] Explicit, opt-in timestamp rebasing helper — `read()` itself keeps returning
|
|
55
|
+
timestamps exactly as recorded
|
|
56
|
+
- [ ] J1939 arbitration-ID decomposition (priority, PGN, source address): pure per-frame
|
|
57
|
+
arithmetic with no DBC awareness, which is where dbckit's J1939 helpers deliberately
|
|
58
|
+
stop
|
|
59
|
+
|
|
60
|
+
## CLI (`capkit[cli]`)
|
|
61
|
+
|
|
62
|
+
- [ ] `probe`, `head`, and `stats` commands (unique IDs, per-ID counts, time span)
|
|
63
|
+
- [ ] `convert` between supported formats once writers exist
|
|
64
|
+
|
|
65
|
+
## Deferred additions
|
|
66
|
+
|
|
67
|
+
- [ ] Writers and format conversion
|
|
68
|
+
- [ ] Dataframe export (pandas/polars) — recipes first, optional helpers only if the
|
|
69
|
+
recipes prove insufficient
|
|
70
|
+
- [ ] python-can `Message` interop helpers
|
|
71
|
+
- [ ] Benchmark suite pinning parse throughput on large logs
|
|
@@ -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
|
+
]
|