structo 0.0.8__tar.gz → 0.0.10__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.
- structo-0.0.10/PKG-INFO +17 -0
- structo-0.0.10/docs/examples/serializer.md +33 -0
- structo-0.0.8/examples/wav.py → structo-0.0.10/docs/examples/wav.md +9 -4
- structo-0.0.10/docs/index.md +14 -0
- structo-0.0.10/docs/interfaces/serialisable.md +68 -0
- structo-0.0.10/docs/interfaces/serializer.md +39 -0
- structo-0.0.10/docs/security.md +4 -0
- structo-0.0.10/docs/types/array.md +3 -0
- structo-0.0.10/docs/types/blob.md +13 -0
- structo-0.0.10/docs/types/buffer.md +14 -0
- structo-0.0.10/docs/types/list.md +13 -0
- structo-0.0.10/docs/types/literal.md +13 -0
- structo-0.0.10/docs/types/numbers.md +48 -0
- structo-0.0.10/docs/types/packed-ints.md +15 -0
- structo-0.0.10/docs/types/serializable-object.md +8 -0
- structo-0.0.10/docs/types/string.md +13 -0
- structo-0.0.10/examples/custom_serializer.py +37 -0
- {structo-0.0.8 → structo-0.0.10}/examples/iterable_serializer.py +8 -7
- structo-0.0.10/examples/wav.py +39 -0
- structo-0.0.10/mkdocs.yml +88 -0
- structo-0.0.10/pyproject.toml +18 -0
- {structo-0.0.8 → structo-0.0.10}/structo/__init__.py +5 -10
- structo-0.0.10/structo/interfaces.py +46 -0
- structo-0.0.10/structo/objects/__init__.py +2 -0
- structo-0.0.10/structo/objects/packed.py +50 -0
- structo-0.0.10/structo/objects/serializable_object.py +37 -0
- {structo-0.0.8 → structo-0.0.10}/structo/serialise.py +2 -2
- {structo-0.0.8/structo/types → structo-0.0.10/structo/serializers}/__init__.py +2 -1
- structo-0.0.10/structo/serializers/array.py +34 -0
- structo-0.0.10/structo/serializers/blob.py +20 -0
- structo-0.0.10/structo/serializers/buffer.py +21 -0
- structo-0.0.10/structo/serializers/list.py +29 -0
- {structo-0.0.8/structo/types → structo-0.0.10/structo/serializers}/literal.py +9 -9
- structo-0.0.8/structo/types/primatives.py → structo-0.0.10/structo/serializers/numbers.py +5 -6
- {structo-0.0.8/structo/types → structo-0.0.10/structo/serializers}/object.py +7 -6
- structo-0.0.10/structo/serializers/optional.py +39 -0
- {structo-0.0.8/structo/types → structo-0.0.10/structo/serializers}/packed.py +16 -18
- structo-0.0.10/structo/serializers/string.py +27 -0
- {structo-0.0.8 → structo-0.0.10}/tests/test_array.py +2 -2
- {structo-0.0.8 → structo-0.0.10}/tests/test_list.py +1 -1
- {structo-0.0.8 → structo-0.0.10}/tests/utils.py +0 -9
- structo-0.0.8/PKG-INFO +0 -8
- structo-0.0.8/examples/custom_serializer.py +0 -52
- structo-0.0.8/pyproject.toml +0 -19
- structo-0.0.8/structo/object.py +0 -58
- structo-0.0.8/structo/packed.py +0 -78
- structo-0.0.8/structo/serializer.py +0 -26
- structo-0.0.8/structo/types/array.py +0 -34
- structo-0.0.8/structo/types/blob.py +0 -19
- structo-0.0.8/structo/types/buffer.py +0 -21
- structo-0.0.8/structo/types/list.py +0 -32
- structo-0.0.8/structo/types/string.py +0 -21
- structo-0.0.8/structo/utils.py +0 -95
- {structo-0.0.8 → structo-0.0.10}/.gitignore +0 -0
- {structo-0.0.8 → structo-0.0.10}/LICENSE +0 -0
- {structo-0.0.8 → structo-0.0.10}/README.md +0 -0
- {structo-0.0.8 → structo-0.0.10}/pytest.ini +0 -0
- {structo-0.0.8 → structo-0.0.10}/tests/test_literal.py +0 -0
- {structo-0.0.8 → structo-0.0.10}/tests/test_object.py +0 -0
- {structo-0.0.8 → structo-0.0.10}/tests/test_packed.py +0 -0
- {structo-0.0.8 → structo-0.0.10}/tests/test_primatives.py +0 -0
- {structo-0.0.8 → structo-0.0.10}/tests/test_size.py +0 -0
- {structo-0.0.8 → structo-0.0.10}/uv.lock +0 -0
structo-0.0.10/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: structo
|
|
3
|
+
Version: 0.0.10
|
|
4
|
+
Summary: Structify
|
|
5
|
+
Author-email: Ben Brady <benbradybusiness@gmail.com>
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: black>=26.1.0 ; extra == "dev"
|
|
10
|
+
Requires-Dist: pytest>=9.0.2 ; extra == "dev"
|
|
11
|
+
Requires-Dist: black ; extra == "docs"
|
|
12
|
+
Requires-Dist: mkdocs ; extra == "docs"
|
|
13
|
+
Requires-Dist: mkdocs-material ; extra == "docs"
|
|
14
|
+
Requires-Dist: mkdocstrings[crystal, python] ; extra == "docs"
|
|
15
|
+
Project-URL: Home, https://nnilky.site
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Provides-Extra: docs
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
## Example
|
|
2
|
+
|
|
3
|
+
## Optional Int Serializer
|
|
4
|
+
```py
|
|
5
|
+
from
|
|
6
|
+
|
|
7
|
+
NONE_TYPE_BYTE = bytes([0])
|
|
8
|
+
INT_TYPE_BYTE = bytes([0])
|
|
9
|
+
|
|
10
|
+
class OptionalIntSerializer(Serializer[OptionalInt]):
|
|
11
|
+
_int_type: Serializer[int]
|
|
12
|
+
|
|
13
|
+
def __init__(self, int_type: Serializer[int]):
|
|
14
|
+
self._int_type = int_type
|
|
15
|
+
|
|
16
|
+
def write(self, f, value):
|
|
17
|
+
if value is None:
|
|
18
|
+
f.write(NONE_TYPE_BYTE)
|
|
19
|
+
else:
|
|
20
|
+
assert type(value) is int, "Value was not int"
|
|
21
|
+
f.write(INT_TYPE_BYTE)
|
|
22
|
+
f.write(self._int_type.write(value))
|
|
23
|
+
|
|
24
|
+
def read(self, f):
|
|
25
|
+
type_byte = f.read(1)
|
|
26
|
+
if type_byte == NONE_TYPE_BYTE:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
if type_byte != INT_TYPE_BYTE:
|
|
30
|
+
raise ValueError(f"Invalid type byte: {type_byte}")
|
|
31
|
+
|
|
32
|
+
return self._int_type.write(f)
|
|
33
|
+
```
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# Wav File
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
```
|
|
5
|
+
import io
|
|
1
6
|
from typing import Annotated
|
|
2
7
|
from structo import uint16_LE, uint32_LE, SerializableObject, Literal
|
|
3
8
|
|
|
@@ -22,11 +27,11 @@ class WavFormat(SerializableObject):
|
|
|
22
27
|
bits_per_sample: Annotated[int, uint16_LE]
|
|
23
28
|
|
|
24
29
|
|
|
25
|
-
|
|
30
|
+
def read_wav_file(f: io.Reader) -> tuple[WavFormat, bytes]:
|
|
26
31
|
WavHeader.read(f)
|
|
27
32
|
|
|
28
33
|
format_header = ChunkHeader.read(f)
|
|
29
|
-
assert format_header.id == b
|
|
34
|
+
assert format_header.id == b"fmt "
|
|
30
35
|
|
|
31
36
|
format_data = f.read(format_header.size)
|
|
32
37
|
format = WavFormat.from_bytes(format_data)
|
|
@@ -34,6 +39,6 @@ with open("example.wav", "rb") as f:
|
|
|
34
39
|
data_header = ChunkHeader.read(f)
|
|
35
40
|
assert data_header.id == b"data"
|
|
36
41
|
wav_data = f.read(data_header.size)
|
|
42
|
+
return format, wav_data
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
print(len(wav_data))
|
|
44
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Serializable
|
|
2
|
+
|
|
3
|
+
In order for classes (such as PackedInts) to be serialiable without having to annotate a serializer, they implement `Serializable`.
|
|
4
|
+
|
|
5
|
+
This is just means they have a method that returns a serialiazer based on thier class.
|
|
6
|
+
|
|
7
|
+
```py
|
|
8
|
+
class Serializable:
|
|
9
|
+
@classmethod
|
|
10
|
+
def serializer(cls) -> Serializer[t.Self]: ...
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For example, for Serili
|
|
14
|
+
```py
|
|
15
|
+
from .serializers import ObjectSerializer
|
|
16
|
+
|
|
17
|
+
class SerializableObject(Serializable):
|
|
18
|
+
@classmethod
|
|
19
|
+
def serializer(cls) -> Serializer[t.Self]:
|
|
20
|
+
return ObjectSerializer(cls)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
|
|
25
|
+
### Basic Serializable
|
|
26
|
+
|
|
27
|
+
```py
|
|
28
|
+
from structo import Serializer, Serializable, SerializableObject
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class UserFlags(Serializable):
|
|
33
|
+
is_alive: bool
|
|
34
|
+
is_banned: bool
|
|
35
|
+
is_admin: bool
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def serializer(cls):
|
|
39
|
+
return UserFlagsSerializer()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class UserFlagsSerializer(Serializer[UserFlags]):
|
|
43
|
+
def write(self, f, value):
|
|
44
|
+
byte = (
|
|
45
|
+
int(value.is_alive) << 0 +
|
|
46
|
+
int(value.is_admin) << 1 +
|
|
47
|
+
int(value.is_banned) << 2
|
|
48
|
+
)
|
|
49
|
+
f.write(bytes([byte]))
|
|
50
|
+
|
|
51
|
+
def read(self, f):
|
|
52
|
+
byte = f.read(1)[0]
|
|
53
|
+
return UserFlags(
|
|
54
|
+
is_alive = ((byte >> 0) & 1) == 1,
|
|
55
|
+
is_admin = ((byte >> 1) & 1) == 1,
|
|
56
|
+
is_banned = ((byte >> 2) & 1) == 1
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class User(SerializableObject):
|
|
61
|
+
name: str
|
|
62
|
+
flags: UserFlags
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Note: in this example you could use a packed ints instead
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## Example
|
|
2
|
+
|
|
3
|
+
## Optional Int Serializer
|
|
4
|
+
```py
|
|
5
|
+
from
|
|
6
|
+
|
|
7
|
+
NONE_TYPE_BYTE = bytes([0])
|
|
8
|
+
INT_TYPE_BYTE = bytes([0])
|
|
9
|
+
|
|
10
|
+
class OptionalIntSerializer(Serializer[OptionalInt]):
|
|
11
|
+
_int_type: Serializer[int]
|
|
12
|
+
|
|
13
|
+
def __init__(self, int_type: Serializer[int]):
|
|
14
|
+
self._int_type = int_type
|
|
15
|
+
|
|
16
|
+
def write(self, f, value):
|
|
17
|
+
if value is None:
|
|
18
|
+
f.write(NONE_TYPE_BYTE)
|
|
19
|
+
else:
|
|
20
|
+
assert type(value) is int, "Value was not int"
|
|
21
|
+
f.write(INT_TYPE_BYTE)
|
|
22
|
+
f.write(self._int_type.write(value))
|
|
23
|
+
|
|
24
|
+
def read(self, f):
|
|
25
|
+
type_byte = f.read(1)
|
|
26
|
+
if type_byte == NONE_TYPE_BYTE:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
if type_byte != INT_TYPE_BYTE:
|
|
30
|
+
raise ValueError(f"Invalid type byte: {type_byte}")
|
|
31
|
+
|
|
32
|
+
return self._int_type.write(f)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Security
|
|
36
|
+
|
|
37
|
+
You should always treat any data parsed as untrusted and validate this.
|
|
38
|
+
|
|
39
|
+
If a value should be in a set of contrained values, add an assert.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Blob
|
|
2
|
+
|
|
3
|
+
A dynamically set of bytes that is prefixed with it's length
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
When creating your own system, always use a uint number. Signed intergers are allows for compatiblity with existing systems
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
```py
|
|
12
|
+
Blob(uint32_BE) # Blob with length stored as uint32bit
|
|
13
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Buffer
|
|
2
|
+
|
|
3
|
+
A fixed length of atritrary bytes
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
```py
|
|
8
|
+
foo: Buffer(100) # The next 100 bytes
|
|
9
|
+
|
|
10
|
+
class Chunk(SerializableObject):
|
|
11
|
+
chunk_id: Annotated[int, uint32_LE]
|
|
12
|
+
checksum: Annotated[bytes, Buffer(8)]
|
|
13
|
+
data: Annotated[bytes, Buffer(4096 - 8 - 4)]
|
|
14
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Byte Literal
|
|
2
|
+
|
|
3
|
+
It's a common pattern to have a fixed literal in a file format, Byte Literal lets you implements this more simply.
|
|
4
|
+
|
|
5
|
+
## Example
|
|
6
|
+
|
|
7
|
+
```py
|
|
8
|
+
from structo import Literal
|
|
9
|
+
filetype: Literal(b"mp4")
|
|
10
|
+
|
|
11
|
+
# You can specify multiple allowed values
|
|
12
|
+
chunk_type: Literal(b"data", b"head", b"fmt ")
|
|
13
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Number Types
|
|
2
|
+
|
|
3
|
+
## Endianness
|
|
4
|
+
|
|
5
|
+
Multi-byte integers have a big endian (`_BE`) and a little endiant (`_LE`) for their different
|
|
6
|
+
[endianness](https://en.wikipedia.org/wiki/Endianness). This determines the order the bytes
|
|
7
|
+
are stored
|
|
8
|
+
|
|
9
|
+
Modern applications should use **little endian**, however lots of file formats and network
|
|
10
|
+
protocols use **big endian**. Make sure to double check when implementing a protocol.
|
|
11
|
+
|
|
12
|
+
## Unsigned Integers
|
|
13
|
+
|
|
14
|
+
Unsigned integers can only be positive.
|
|
15
|
+
|
|
16
|
+
| Type | Bytes | Range | Endianness |
|
|
17
|
+
| ------------------- | ----- | ----------- | ---------- |
|
|
18
|
+
| `structo.uint8` | 1 | 0 to 255 | None |
|
|
19
|
+
| `structo.uint16_LE` | 2 | 0 to 65.5k | little |
|
|
20
|
+
| `structo.uint16_BE` | 2 | 0 to 65.5k | big |
|
|
21
|
+
| `structo.uint32_LE` | 4 | 0 to 4.2B | little |
|
|
22
|
+
| `structo.uint32_BE` | 4 | 0 to 4.2B | big |
|
|
23
|
+
| `structo.uint64_LE` | 8 | 0 to 1.8e19 | little |
|
|
24
|
+
| `structo.uint64_BE` | 8 | 0 to 1.8e19 | big |
|
|
25
|
+
|
|
26
|
+
## Signed Integers
|
|
27
|
+
|
|
28
|
+
Signed integers allow negative values and are stored as
|
|
29
|
+
[two's compliment](https://en.wikipedia.org/wiki/Two's_complement).
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
| Type | Bytes | Range | Endianness |
|
|
33
|
+
| ------------------ | ----- | --------------- | ---------- |
|
|
34
|
+
| `structo.int8` | 1 | -128 to 127 | None |
|
|
35
|
+
| `structo.int16_LE` | 2 | -32.7k to 32.7k | little |
|
|
36
|
+
| `structo.int16_BE` | 2 | -32.7k to 32.7k | big |
|
|
37
|
+
| `structo.int32_LE` | 4 | -2.1B to 2.1B | little |
|
|
38
|
+
| `structo.int32_BE` | 4 | -2.1B to 2.1B | big |
|
|
39
|
+
| `structo.int64_LE` | 8 | -9e18 to 9e18 | little |
|
|
40
|
+
| `structo.int64_BE` | 8 | -9e18 to 9e18 | big |
|
|
41
|
+
|
|
42
|
+
## Floats
|
|
43
|
+
|
|
44
|
+
| Type | Bytes | Endianness |
|
|
45
|
+
| ------------------ | ----- | ---------- |
|
|
46
|
+
| `structo.float32` | 4 | N/A |
|
|
47
|
+
| `structo.float64` | 8 | N/A |
|
|
48
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# PackedInts
|
|
2
|
+
|
|
3
|
+
PackedInts lets you pack bits into fewer bytes.
|
|
4
|
+
|
|
5
|
+
It's total
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
```py
|
|
9
|
+
import structo as st
|
|
10
|
+
|
|
11
|
+
class Foo(st.PackedInts):
|
|
12
|
+
type: t.Annotated[int, st.PackedInt(bits=2)]
|
|
13
|
+
completed: t.Annotated[int, st.PackedInt(bits=1)]
|
|
14
|
+
b: t.Annotated[int, st.PackedInt(bits=5)]
|
|
15
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# String
|
|
2
|
+
|
|
3
|
+
A UTF-8 encoded string with a dynamic length, the length is stored as a prefix.
|
|
4
|
+
|
|
5
|
+
The length is uint32 by default, meaning a max length of 4.2M characters.
|
|
6
|
+
|
|
7
|
+
## Examples
|
|
8
|
+
|
|
9
|
+
```py
|
|
10
|
+
String() # uint32 by default, max length 4.2GB
|
|
11
|
+
String(uint16_LE) # max length 65,536
|
|
12
|
+
String(uint8) # max length 255
|
|
13
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from structo import Serializer, Serializable, SerializableObject, String
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class UserFlags(Serializable):
|
|
8
|
+
is_alive: bool
|
|
9
|
+
is_banned: bool
|
|
10
|
+
is_admin: bool
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def serializer(cls):
|
|
14
|
+
return UserFlagsSerializer()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UserFlagsSerializer(Serializer[UserFlags]):
|
|
18
|
+
def write(self, f, value):
|
|
19
|
+
byte = (
|
|
20
|
+
int(value.is_alive) << 0 |
|
|
21
|
+
int(value.is_admin) << 1 |
|
|
22
|
+
int(value.is_banned) << 2
|
|
23
|
+
)
|
|
24
|
+
f.write(bytes([byte]))
|
|
25
|
+
|
|
26
|
+
def read(self, f):
|
|
27
|
+
byte = f.read(1)[0]
|
|
28
|
+
return UserFlags(
|
|
29
|
+
is_alive = ((byte >> 0) & 1) == 1,
|
|
30
|
+
is_admin = ((byte >> 1) & 1) == 1,
|
|
31
|
+
is_banned = ((byte >> 2) & 1) == 1
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class User(SerializableObject):
|
|
36
|
+
name: Annotated[str, String()]
|
|
37
|
+
flags: UserFlags
|
|
@@ -21,22 +21,23 @@ NULL_TERMINATOR = bytes([0])
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class PostsSerialiser(Serializer[t.Iterable[Post]]):
|
|
24
|
-
def write(self,
|
|
24
|
+
def write(self, f, value):
|
|
25
25
|
for item in value:
|
|
26
|
-
|
|
27
|
-
item.write(
|
|
26
|
+
f.write(CONTINUE_BYTE)
|
|
27
|
+
item.write(f)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
f.write(NULL_TERMINATOR)
|
|
30
30
|
|
|
31
|
-
def read(self,
|
|
31
|
+
def read(self, f):
|
|
32
32
|
while True:
|
|
33
|
-
continue_byte =
|
|
33
|
+
continue_byte = f.read(1)
|
|
34
34
|
if continue_byte == NULL_TERMINATOR:
|
|
35
35
|
break
|
|
36
|
+
|
|
36
37
|
assert continue_byte == CONTINUE_BYTE, "Continue byte was not 255"
|
|
37
38
|
|
|
38
39
|
print("Loading...") # to prove it's interspliced loading and yielding
|
|
39
|
-
yield Post.read(
|
|
40
|
+
yield Post.read(f)
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
type PostsIterable = t.Annotated[t.Iterable[Post], PostsSerialiser()]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import typing as t
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
from structo import uint16_LE, uint32_LE, SerializableObject, Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class WavHeader(SerializableObject):
|
|
8
|
+
chunk_id: Annotated[t.Literal[b"RIFF"], Literal(b"RIFF")]
|
|
9
|
+
file_size: Annotated[int, uint32_LE]
|
|
10
|
+
format: Annotated[t.Literal[b"WAVE"], Literal(b"WAVE")]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChunkHeader(SerializableObject):
|
|
14
|
+
id: Annotated[t.Literal[b"fmt ", b"data"], Literal(b"fmt ", b"data")]
|
|
15
|
+
size: Annotated[int, uint32_LE]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WavFormat(SerializableObject):
|
|
19
|
+
audio_format: Annotated[int, uint16_LE]
|
|
20
|
+
num_channels: Annotated[int, uint16_LE]
|
|
21
|
+
sample_rate: Annotated[int, uint32_LE]
|
|
22
|
+
byte_range: Annotated[int, uint32_LE]
|
|
23
|
+
block_align: Annotated[int, uint16_LE]
|
|
24
|
+
bits_per_sample: Annotated[int, uint16_LE]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def read_wav_file(f: io.Reader) -> tuple[WavFormat, bytes]:
|
|
28
|
+
print(WavHeader.read(f))
|
|
29
|
+
|
|
30
|
+
format_header = ChunkHeader.read(f)
|
|
31
|
+
assert format_header.id == b"fmt "
|
|
32
|
+
|
|
33
|
+
format_data = f.read(format_header.size)
|
|
34
|
+
format = WavFormat.from_bytes(format_data)
|
|
35
|
+
|
|
36
|
+
data_header = ChunkHeader.read(f)
|
|
37
|
+
assert data_header.id == b"data"
|
|
38
|
+
wav_data = f.read(data_header.size)
|
|
39
|
+
return format, wav_data
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
site_url: https://ben-brady.github.io/structo/
|
|
2
|
+
docs_dir: docs
|
|
3
|
+
|
|
4
|
+
site_name: Structo
|
|
5
|
+
site_description: Simplified byte serialisation and deseralisation
|
|
6
|
+
site_author: Ben Brady
|
|
7
|
+
repo_name: Structo
|
|
8
|
+
repo_url: https://github.com/Ben-Brady/structo
|
|
9
|
+
|
|
10
|
+
nav:
|
|
11
|
+
- Introduction:
|
|
12
|
+
- Home: index.md
|
|
13
|
+
- Quickstart: introduction/quickstart.md
|
|
14
|
+
- Data Types:
|
|
15
|
+
- Numbers: types/numbers.md
|
|
16
|
+
- Buffer: types/buffer.md
|
|
17
|
+
- Blob: types/blob.md
|
|
18
|
+
- String: types/string.md
|
|
19
|
+
- Byte Literals: types/literal.md
|
|
20
|
+
- Container Types:
|
|
21
|
+
- Serializable Object: types/serializable-object.md
|
|
22
|
+
- Array: types/array.md
|
|
23
|
+
- List: types/list.md
|
|
24
|
+
- Packed Ints: types/packed-ints.md
|
|
25
|
+
- Security: security.md
|
|
26
|
+
|
|
27
|
+
theme:
|
|
28
|
+
name: material
|
|
29
|
+
# favicon: assets/logo-monochrome.svg
|
|
30
|
+
# logo: assets/logo.svg
|
|
31
|
+
features:
|
|
32
|
+
- content.code.copy # Add the copy to clipboard button to code blocks
|
|
33
|
+
- search # Add the search bar
|
|
34
|
+
- search.suggest # Add suggestions to search
|
|
35
|
+
- navigation.top
|
|
36
|
+
- navigation.tabs # Page tabs at the top
|
|
37
|
+
- navigation.instant # Preload pages
|
|
38
|
+
- navigation.tracking # Add anchor tag to url
|
|
39
|
+
extra:
|
|
40
|
+
social:
|
|
41
|
+
- icon: fontawesome/brands/github
|
|
42
|
+
link: https://github.com/Ben-Brady/structo
|
|
43
|
+
palette:
|
|
44
|
+
- media: '(prefers-color-scheme: light)'
|
|
45
|
+
scheme: default
|
|
46
|
+
primary: green
|
|
47
|
+
accent: amber
|
|
48
|
+
toggle:
|
|
49
|
+
icon: material/lightbulb
|
|
50
|
+
name: Switch to dark mode
|
|
51
|
+
- media: '(prefers-color-scheme: dark)'
|
|
52
|
+
scheme: slate
|
|
53
|
+
primary: green
|
|
54
|
+
accent: amber
|
|
55
|
+
toggle:
|
|
56
|
+
icon: material/lightbulb-outline
|
|
57
|
+
name: Switch to light mode
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
plugins:
|
|
61
|
+
search: # Add the search bar
|
|
62
|
+
offline:
|
|
63
|
+
mkdocstrings:
|
|
64
|
+
handlers:
|
|
65
|
+
python:
|
|
66
|
+
options:
|
|
67
|
+
show_root_heading: true
|
|
68
|
+
show_if_no_docstring: true
|
|
69
|
+
inherited_members: true
|
|
70
|
+
members_order: source
|
|
71
|
+
separate_signature: true
|
|
72
|
+
unwrap_annotated: true
|
|
73
|
+
filters:
|
|
74
|
+
- '!^_'
|
|
75
|
+
merge_init_into_class: true
|
|
76
|
+
docstring_section_style: spacy
|
|
77
|
+
signature_crossrefs: true
|
|
78
|
+
show_symbol_type_heading: true
|
|
79
|
+
show_symbol_type_toc: true
|
|
80
|
+
|
|
81
|
+
markdown_extensions:
|
|
82
|
+
- pymdownx.highlight:
|
|
83
|
+
anchor_linenums: true
|
|
84
|
+
line_spans: __span
|
|
85
|
+
pygments_lang_class: true
|
|
86
|
+
- pymdownx.inlinehilite
|
|
87
|
+
- pymdownx.snippets
|
|
88
|
+
- pymdownx.superfences
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.11,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "structo"
|
|
7
|
+
authors = [{ name = "Ben Brady", email = "benbradybusiness@gmail.com" }]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
dynamic = ["version", "description"]
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
Home = "https://nnilky.site"
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = ["black>=26.1.0", "pytest>=9.0.2"]
|
|
18
|
+
docs = ["black", "mkdocs", "mkdocs-material", "mkdocstrings[crystal,python]"]
|
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
Structify
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
__version__ = "0.0.
|
|
5
|
+
__version__ = "0.0.10"
|
|
6
6
|
|
|
7
|
-
from .
|
|
7
|
+
from .interfaces import Serializer, Serializable
|
|
8
8
|
from .serialise import get_serializer
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
11
|
-
from .types import (
|
|
9
|
+
from .objects import SerializableObject, PackedInt, PackedInts
|
|
10
|
+
from .serializers import (
|
|
12
11
|
uint64_BE,
|
|
13
12
|
uint64_LE,
|
|
14
13
|
uint64,
|
|
@@ -41,9 +40,5 @@ from .types import (
|
|
|
41
40
|
String,
|
|
42
41
|
Blob,
|
|
43
42
|
Literal,
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
from .utils import (
|
|
47
|
-
StructoReader,
|
|
48
|
-
StructifyWriter,
|
|
43
|
+
Optional,
|
|
49
44
|
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Serializable:
|
|
6
|
+
@classmethod
|
|
7
|
+
def serializer(cls) -> Serializer[t.Self]: ...
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def sizeof(cls) -> int | None:
|
|
11
|
+
return cls.serializer().sizeof()
|
|
12
|
+
|
|
13
|
+
def write(self, f: t.IO[bytes]):
|
|
14
|
+
return self.serializer().write(f, self)
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def read(cls, f: t.IO[bytes]) -> t.Self:
|
|
18
|
+
return cls.serializer().read(f)
|
|
19
|
+
|
|
20
|
+
def to_bytes(self) -> bytes:
|
|
21
|
+
return self.serializer().to_bytes(self)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_bytes(cls, data: bytes) -> t.Self:
|
|
25
|
+
return cls.serializer().from_bytes(data)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Serializer[T]:
|
|
29
|
+
def sizeof(self) -> int | None:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def write(self, f: t.IO[bytes], value: T):
|
|
33
|
+
raise NotImplementedError(f"{type(self).__name__} can't be serialized")
|
|
34
|
+
|
|
35
|
+
def read(self, f: t.IO[bytes]) -> T:
|
|
36
|
+
raise NotImplementedError(f"{type(self).__name__} can't be deserialized")
|
|
37
|
+
|
|
38
|
+
def to_bytes(self, value: T) -> bytes:
|
|
39
|
+
buf = io.BytesIO()
|
|
40
|
+
self.write(buf, value)
|
|
41
|
+
buf.seek(0)
|
|
42
|
+
return buf.getvalue()
|
|
43
|
+
|
|
44
|
+
def from_bytes(self, data: bytes) -> T:
|
|
45
|
+
buf = io.BytesIO(data)
|
|
46
|
+
return self.read(buf)
|