bytestruct 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 (36) hide show
  1. bytestruct-0.1.0/.github/workflows/docs.yml +60 -0
  2. bytestruct-0.1.0/.github/workflows/python-app.yml +73 -0
  3. bytestruct-0.1.0/.github/workflows/python-publish.yml +29 -0
  4. bytestruct-0.1.0/.gitignore +13 -0
  5. bytestruct-0.1.0/.pre-commit-config.yaml +20 -0
  6. bytestruct-0.1.0/LICENSE +21 -0
  7. bytestruct-0.1.0/PKG-INFO +127 -0
  8. bytestruct-0.1.0/README.md +91 -0
  9. bytestruct-0.1.0/bytestruct/__init__.py +1088 -0
  10. bytestruct-0.1.0/bytestruct/_version.py +24 -0
  11. bytestruct-0.1.0/bytestruct/exceptions.py +76 -0
  12. bytestruct-0.1.0/bytestruct/py.typed +0 -0
  13. bytestruct-0.1.0/bytestruct.egg-info/PKG-INFO +127 -0
  14. bytestruct-0.1.0/bytestruct.egg-info/SOURCES.txt +34 -0
  15. bytestruct-0.1.0/bytestruct.egg-info/dependency_links.txt +1 -0
  16. bytestruct-0.1.0/bytestruct.egg-info/requires.txt +13 -0
  17. bytestruct-0.1.0/bytestruct.egg-info/scm_file_list.json +30 -0
  18. bytestruct-0.1.0/bytestruct.egg-info/scm_version.json +8 -0
  19. bytestruct-0.1.0/bytestruct.egg-info/top_level.txt +1 -0
  20. bytestruct-0.1.0/docs/api.md +12 -0
  21. bytestruct-0.1.0/docs/byte-reader.md +160 -0
  22. bytestruct-0.1.0/docs/codecs.md +331 -0
  23. bytestruct-0.1.0/docs/cookbook.md +398 -0
  24. bytestruct-0.1.0/docs/errors.md +217 -0
  25. bytestruct-0.1.0/docs/extending.md +581 -0
  26. bytestruct-0.1.0/docs/getting-started.md +85 -0
  27. bytestruct-0.1.0/docs/how-it-works.md +227 -0
  28. bytestruct-0.1.0/docs/index.md +78 -0
  29. bytestruct-0.1.0/docs/migrating-from-construct.md +208 -0
  30. bytestruct-0.1.0/docs/records.md +458 -0
  31. bytestruct-0.1.0/docs/reference.md +666 -0
  32. bytestruct-0.1.0/mkdocs.yml +117 -0
  33. bytestruct-0.1.0/pyproject.toml +148 -0
  34. bytestruct-0.1.0/setup.cfg +4 -0
  35. bytestruct-0.1.0/tests/test_bytestruct.py +593 -0
  36. bytestruct-0.1.0/tests/test_docs_examples.py +79 -0
@@ -0,0 +1,60 @@
1
+ name: docs
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ paths:
7
+ - "docs/**"
8
+ - "mkdocs.yml"
9
+ - "bytestruct/**"
10
+ - "pyproject.toml"
11
+ - ".github/workflows/docs.yml"
12
+ pull_request:
13
+ paths:
14
+ - "docs/**"
15
+ - "mkdocs.yml"
16
+ - "bytestruct/**"
17
+ - "pyproject.toml"
18
+ - ".github/workflows/docs.yml"
19
+ workflow_dispatch:
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ jobs:
25
+ build:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ with:
30
+ # setuptools_scm derives the version from the tags.
31
+ fetch-depth: 0
32
+ - uses: actions/setup-python@v5
33
+ with:
34
+ python-version: "3.12"
35
+ - name: Install
36
+ run: |
37
+ python -m pip install --upgrade pip
38
+ python -m pip install -e ".[docs]"
39
+ - name: Build (strict)
40
+ run: python -m mkdocs build --strict
41
+ - name: Upload site artifact
42
+ if: github.ref == 'refs/heads/master'
43
+ uses: actions/upload-pages-artifact@v3
44
+ with:
45
+ path: site
46
+
47
+ deploy:
48
+ # Only deploy from master; PRs just run the strict build above as a check.
49
+ if: github.ref == 'refs/heads/master'
50
+ needs: build
51
+ runs-on: ubuntu-latest
52
+ permissions:
53
+ pages: write
54
+ id-token: write
55
+ environment:
56
+ name: github-pages
57
+ url: ${{ steps.deployment.outputs.page_url }}
58
+ steps:
59
+ - id: deployment
60
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,73 @@
1
+ name: Python application
2
+
3
+ on:
4
+ push:
5
+ branches: [ "master" ]
6
+ pull_request:
7
+ branches: [ "master" ]
8
+ release:
9
+ types: [ created ]
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ test:
17
+ if: '! github.event.pull_request.draft'
18
+ runs-on: ${{ matrix.os }}
19
+
20
+ # Pin uv to the interpreter it provisioned instead of an already-installed system Python.
21
+ env:
22
+ UV_PYTHON_PREFERENCE: only-managed
23
+
24
+ strategy:
25
+ fail-fast: false
26
+ matrix:
27
+ # 3.15 is a prerelease (uv resolves it to the latest beta).
28
+ python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15" ]
29
+ os: [ ubuntu-latest, macos-latest, windows-latest ]
30
+
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ # setuptools_scm derives the version from the tags.
35
+ fetch-depth: 0
36
+ # uv provisions the interpreter (python-build-standalone): unlike actions/setup-python,
37
+ # it still provides 3.9 on current runner images (ubuntu-24.04, arm64 macOS).
38
+ - name: Install uv and Python ${{ matrix.python-version }}
39
+ uses: astral-sh/setup-uv@v7
40
+ with:
41
+ python-version: ${{ matrix.python-version }}
42
+ enable-cache: true
43
+ cache-dependency-glob: |
44
+ pyproject.toml
45
+ - name: Run pre-commit hooks
46
+ # pyright needs the project installed; it runs in the dedicated job below.
47
+ env:
48
+ SKIP: pyright
49
+ run: uvx pre-commit run --all-files
50
+ - name: Run tests
51
+ run: uv run --extra test pytest
52
+
53
+ pyright:
54
+ if: '! github.event.pull_request.draft'
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+ with:
59
+ fetch-depth: 0
60
+ - name: Install uv
61
+ uses: astral-sh/setup-uv@v7
62
+ with:
63
+ python-version: "3.14"
64
+ enable-cache: true
65
+ cache-dependency-glob: |
66
+ pyproject.toml
67
+ - name: Install dependencies
68
+ run: |
69
+ uv venv
70
+ uv pip install -e ".[test]"
71
+ # Runs the same pre-commit hook contributors get locally (pinned in .pre-commit-config.yaml).
72
+ - name: Run pyright
73
+ run: uvx pre-commit run pyright --all-files
@@ -0,0 +1,29 @@
1
+ # This workflow builds and uploads the Python package to PyPI when a release is created
2
+
3
+ name: Upload Python Package
4
+
5
+ on:
6
+ release:
7
+ types: [ created ]
8
+
9
+ jobs:
10
+ deploy:
11
+ permissions:
12
+ id-token: write
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with:
17
+ # setuptools_scm needs full history + tags to derive the release version
18
+ # from the tag; a shallow clone yields a PyPI-rejected local version
19
+ # like 0.1.dev1+g<hash>.
20
+ fetch-depth: 0
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v7
23
+ - name: Build distributions
24
+ run: uv build
25
+ - name: Publish to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
27
+ with:
28
+ packages-dir: dist
29
+ verbose: true
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ .idea/
3
+ .venv*/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.pyc
8
+ __pycache__/
9
+ *.egg-info/
10
+ bytestruct/_version.py
11
+ .python-version
12
+ uv.lock
13
+ site/
@@ -0,0 +1,20 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ # Ruff version.
4
+ rev: v0.15.4
5
+ hooks:
6
+ # Run the linter.
7
+ - id: ruff-check
8
+ # Run the formatter.
9
+ - id: ruff-format
10
+ - repo: local
11
+ hooks:
12
+ # Pinned so local hooks and CI agree; bump deliberately together with fixing any new findings.
13
+ # Resolves the project's dependencies from .venv (venvPath/venv in pyproject.toml); CI's test
14
+ # matrix skips this hook (SKIP=pyright) — the dedicated pyright CI job runs it after creating the venv.
15
+ - id: pyright
16
+ name: pyright
17
+ entry: uvx pyright@1.1.411
18
+ language: system
19
+ types: [python]
20
+ pass_filenames: false
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 doronz88
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.
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: bytestruct
3
+ Version: 0.1.0
4
+ Summary: Declarative binary records and composable codecs, using only the standard library
5
+ Author-email: doronz88 <doron88@gmail.com>
6
+ Maintainer-email: doronz88 <doron88@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/doronz88/bytestruct
9
+ Project-URL: Documentation, https://doronz88.github.io/bytestruct/
10
+ Project-URL: Bug Reports, https://github.com/doronz88/bytestruct/issues
11
+ Keywords: binary,struct,parsing,serialization,dataclasses,construct
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: typing_extensions>=4.1.0; python_version < "3.11"
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest; extra == "test"
31
+ Provides-Extra: docs
32
+ Requires-Dist: mkdocs-material>=9.5; python_version >= "3.10" and extra == "docs"
33
+ Requires-Dist: mkdocstrings[python]>=0.26; python_version >= "3.10" and extra == "docs"
34
+ Requires-Dist: mkdocs-llmstxt>=0.5; python_version >= "3.10" and extra == "docs"
35
+ Dynamic: license-file
36
+
37
+ # bytestruct
38
+
39
+ [![Python application](https://github.com/doronz88/bytestruct/workflows/Python%20application/badge.svg)](https://github.com/doronz88/bytestruct/actions/workflows/python-app.yml "Python application action")
40
+ [![Pypi version](https://img.shields.io/pypi/v/bytestruct.svg)](https://pypi.org/project/bytestruct/ "PyPi package")
41
+ [![Docs](https://github.com/doronz88/bytestruct/actions/workflows/docs.yml/badge.svg)](https://doronz88.github.io/bytestruct/ "Documentation")
42
+
43
+ `bytestruct` parses and builds binary wire formats with the standard library alone. Fixed-size records are
44
+ dataclasses whose field annotations give each field's wire type; variable-length data is described by small
45
+ codecs that compose like [construct](https://github.com/construct/construct)'s. Everything is fully typed.
46
+
47
+ The layout of a record is compiled into `struct.Struct` objects once, when the class is defined, so parsing a
48
+ record is a single `unpack_from` per byte-order run.
49
+
50
+ **Documentation: <https://doronz88.github.io/bytestruct/>**, including a
51
+ [migration guide from construct](https://doronz88.github.io/bytestruct/migrating-from-construct/).
52
+
53
+ ## Install
54
+
55
+ ```shell
56
+ python3 -m pip install -U bytestruct
57
+ ```
58
+
59
+ ## Fixed-size records
60
+
61
+ ```python
62
+ from dataclasses import dataclass, field
63
+ from enum import IntEnum
64
+ from typing import Annotated
65
+
66
+ from bytestruct import BinaryStruct, Const, PaddedString, u16, u32
67
+
68
+
69
+ class Kind(IntEnum):
70
+ FILE = 1
71
+ DIRECTORY = 2
72
+
73
+
74
+ @dataclass
75
+ class Header(BinaryStruct, byte_order="<"):
76
+ magic: Annotated[u32, Const(0x1F3D5B79)] = field(default=0x1F3D5B79, init=False)
77
+ size: u32
78
+ name: Annotated[str, PaddedString(16)]
79
+ kind: Annotated[Kind, u16] # parsed into the enum type
80
+
81
+
82
+ data = Header(size=1024, name="notes.txt", kind=Kind.FILE).build() # a bad value -> BinaryBuildError
83
+ header = Header.parse(data) # short input -> TruncatedDataError, wrong magic -> UnexpectedConstantError
84
+ assert header.kind is Kind.FILE
85
+ ```
86
+
87
+ Wire fields are positional `__init__` parameters in wire order. A field declared
88
+ `= field(default=..., init=False)` is left out of `__init__`: parse skips its bytes (padding) and build packs
89
+ its default (on a `Const` field, the constant). Fields without a wire type may follow the wire fields and are
90
+ ignored by parse/build. Records cannot be subclassed.
91
+
92
+ Available field types include `u8`..`u64`, `i16`..`i64`, byte-order pinned `u32le`/`u32be`, `Bytes(n)`,
93
+ `PaddedString(n)`, `uuid_t`, `ipv4`/`ipv6`, `Array(element, n)`, int-enum fields and `Const` magics; custom
94
+ ones subclass `Format`.
95
+
96
+ ## Codecs
97
+
98
+ ```python
99
+ from bytestruct import CSTRING, U32_LE, U64_LE, Aligned, Prefixed, Switch
100
+
101
+ STRING = Aligned(4, Prefixed(U32_LE, CSTRING)) # u32 length, NUL-terminated UTF-8, padded to 4 bytes
102
+ OBJECT = Switch(U32_LE, {1: STRING, 2: U64_LE}) # parses to (tag, value)
103
+
104
+ assert OBJECT.parse(OBJECT.build((1, "hi"))) == (1, "hi")
105
+ ```
106
+
107
+ A codec is anything with `read(reader)` and `build(value)`: the combinators (`Prefixed`, `Aligned`,
108
+ `PrefixedArray`, `Tuple`, `Switch`), the leaves (`CSTRING`, `GREEDY_STRING`, `GREEDY_BYTES`, `PASS`), the
109
+ scalars (`U8`, `U16_LE`/`U16_BE`, ..., `F64_BE`), `Bytes(n)`, and record classes themselves. A record field annotated with a codec
110
+ (`name: Annotated[str, CSTRING]`) makes the record variable-length; such fields follow the fixed-size ones.
111
+ Formats with no declarative shape are read by hand with `ByteReader`, whose sub-readers are windows on the same
112
+ buffer (nothing is copied).
113
+
114
+ ## Errors
115
+
116
+ Every failure raises a `BinaryDataError` (a `ValueError`): `BinaryParseError` (with `TruncatedDataError` and
117
+ `UnexpectedConstantError`) or `BinaryBuildError`. Errors carry the record, field, absolute offset and the path
118
+ through nested codecs, and prefix their message with them:
119
+
120
+ ```text
121
+ Header.magic at offset 0: expected 0x1f3d5b79, got 0x0
122
+ DICTIONARY -> [2] -> STRING at offset 104: expected 200 bytes, found 16
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,91 @@
1
+ # bytestruct
2
+
3
+ [![Python application](https://github.com/doronz88/bytestruct/workflows/Python%20application/badge.svg)](https://github.com/doronz88/bytestruct/actions/workflows/python-app.yml "Python application action")
4
+ [![Pypi version](https://img.shields.io/pypi/v/bytestruct.svg)](https://pypi.org/project/bytestruct/ "PyPi package")
5
+ [![Docs](https://github.com/doronz88/bytestruct/actions/workflows/docs.yml/badge.svg)](https://doronz88.github.io/bytestruct/ "Documentation")
6
+
7
+ `bytestruct` parses and builds binary wire formats with the standard library alone. Fixed-size records are
8
+ dataclasses whose field annotations give each field's wire type; variable-length data is described by small
9
+ codecs that compose like [construct](https://github.com/construct/construct)'s. Everything is fully typed.
10
+
11
+ The layout of a record is compiled into `struct.Struct` objects once, when the class is defined, so parsing a
12
+ record is a single `unpack_from` per byte-order run.
13
+
14
+ **Documentation: <https://doronz88.github.io/bytestruct/>**, including a
15
+ [migration guide from construct](https://doronz88.github.io/bytestruct/migrating-from-construct/).
16
+
17
+ ## Install
18
+
19
+ ```shell
20
+ python3 -m pip install -U bytestruct
21
+ ```
22
+
23
+ ## Fixed-size records
24
+
25
+ ```python
26
+ from dataclasses import dataclass, field
27
+ from enum import IntEnum
28
+ from typing import Annotated
29
+
30
+ from bytestruct import BinaryStruct, Const, PaddedString, u16, u32
31
+
32
+
33
+ class Kind(IntEnum):
34
+ FILE = 1
35
+ DIRECTORY = 2
36
+
37
+
38
+ @dataclass
39
+ class Header(BinaryStruct, byte_order="<"):
40
+ magic: Annotated[u32, Const(0x1F3D5B79)] = field(default=0x1F3D5B79, init=False)
41
+ size: u32
42
+ name: Annotated[str, PaddedString(16)]
43
+ kind: Annotated[Kind, u16] # parsed into the enum type
44
+
45
+
46
+ data = Header(size=1024, name="notes.txt", kind=Kind.FILE).build() # a bad value -> BinaryBuildError
47
+ header = Header.parse(data) # short input -> TruncatedDataError, wrong magic -> UnexpectedConstantError
48
+ assert header.kind is Kind.FILE
49
+ ```
50
+
51
+ Wire fields are positional `__init__` parameters in wire order. A field declared
52
+ `= field(default=..., init=False)` is left out of `__init__`: parse skips its bytes (padding) and build packs
53
+ its default (on a `Const` field, the constant). Fields without a wire type may follow the wire fields and are
54
+ ignored by parse/build. Records cannot be subclassed.
55
+
56
+ Available field types include `u8`..`u64`, `i16`..`i64`, byte-order pinned `u32le`/`u32be`, `Bytes(n)`,
57
+ `PaddedString(n)`, `uuid_t`, `ipv4`/`ipv6`, `Array(element, n)`, int-enum fields and `Const` magics; custom
58
+ ones subclass `Format`.
59
+
60
+ ## Codecs
61
+
62
+ ```python
63
+ from bytestruct import CSTRING, U32_LE, U64_LE, Aligned, Prefixed, Switch
64
+
65
+ STRING = Aligned(4, Prefixed(U32_LE, CSTRING)) # u32 length, NUL-terminated UTF-8, padded to 4 bytes
66
+ OBJECT = Switch(U32_LE, {1: STRING, 2: U64_LE}) # parses to (tag, value)
67
+
68
+ assert OBJECT.parse(OBJECT.build((1, "hi"))) == (1, "hi")
69
+ ```
70
+
71
+ A codec is anything with `read(reader)` and `build(value)`: the combinators (`Prefixed`, `Aligned`,
72
+ `PrefixedArray`, `Tuple`, `Switch`), the leaves (`CSTRING`, `GREEDY_STRING`, `GREEDY_BYTES`, `PASS`), the
73
+ scalars (`U8`, `U16_LE`/`U16_BE`, ..., `F64_BE`), `Bytes(n)`, and record classes themselves. A record field annotated with a codec
74
+ (`name: Annotated[str, CSTRING]`) makes the record variable-length; such fields follow the fixed-size ones.
75
+ Formats with no declarative shape are read by hand with `ByteReader`, whose sub-readers are windows on the same
76
+ buffer (nothing is copied).
77
+
78
+ ## Errors
79
+
80
+ Every failure raises a `BinaryDataError` (a `ValueError`): `BinaryParseError` (with `TruncatedDataError` and
81
+ `UnexpectedConstantError`) or `BinaryBuildError`. Errors carry the record, field, absolute offset and the path
82
+ through nested codecs, and prefix their message with them:
83
+
84
+ ```text
85
+ Header.magic at offset 0: expected 0x1f3d5b79, got 0x0
86
+ DICTIONARY -> [2] -> STRING at offset 104: expected 200 bytes, found 16
87
+ ```
88
+
89
+ ## License
90
+
91
+ MIT