structo 0.0.7__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.
Files changed (63) hide show
  1. structo-0.0.10/PKG-INFO +17 -0
  2. structo-0.0.10/docs/examples/serializer.md +33 -0
  3. structo-0.0.7/examples/wav.py → structo-0.0.10/docs/examples/wav.md +9 -4
  4. structo-0.0.10/docs/index.md +14 -0
  5. structo-0.0.10/docs/interfaces/serialisable.md +68 -0
  6. structo-0.0.10/docs/interfaces/serializer.md +39 -0
  7. structo-0.0.10/docs/security.md +4 -0
  8. structo-0.0.10/docs/types/array.md +3 -0
  9. structo-0.0.10/docs/types/blob.md +13 -0
  10. structo-0.0.10/docs/types/buffer.md +14 -0
  11. structo-0.0.10/docs/types/list.md +13 -0
  12. structo-0.0.10/docs/types/literal.md +13 -0
  13. structo-0.0.10/docs/types/numbers.md +48 -0
  14. structo-0.0.10/docs/types/packed-ints.md +15 -0
  15. structo-0.0.10/docs/types/serializable-object.md +8 -0
  16. structo-0.0.10/docs/types/string.md +13 -0
  17. structo-0.0.10/examples/custom_serializer.py +37 -0
  18. {structo-0.0.7 → structo-0.0.10}/examples/iterable_serializer.py +8 -7
  19. structo-0.0.10/examples/wav.py +39 -0
  20. structo-0.0.10/mkdocs.yml +88 -0
  21. structo-0.0.10/pyproject.toml +18 -0
  22. {structo-0.0.7 → structo-0.0.10}/structo/__init__.py +5 -10
  23. structo-0.0.10/structo/interfaces.py +46 -0
  24. structo-0.0.10/structo/objects/__init__.py +2 -0
  25. structo-0.0.10/structo/objects/packed.py +50 -0
  26. structo-0.0.10/structo/objects/serializable_object.py +37 -0
  27. {structo-0.0.7 → structo-0.0.10}/structo/serialise.py +2 -2
  28. {structo-0.0.7/structo/types → structo-0.0.10/structo/serializers}/__init__.py +2 -1
  29. structo-0.0.10/structo/serializers/array.py +34 -0
  30. structo-0.0.10/structo/serializers/blob.py +20 -0
  31. structo-0.0.10/structo/serializers/buffer.py +21 -0
  32. structo-0.0.10/structo/serializers/list.py +29 -0
  33. {structo-0.0.7/structo/types → structo-0.0.10/structo/serializers}/literal.py +9 -9
  34. structo-0.0.7/structo/types/primatives.py → structo-0.0.10/structo/serializers/numbers.py +5 -6
  35. {structo-0.0.7/structo/types → structo-0.0.10/structo/serializers}/object.py +7 -6
  36. structo-0.0.10/structo/serializers/optional.py +39 -0
  37. {structo-0.0.7/structo/types → structo-0.0.10/structo/serializers}/packed.py +16 -18
  38. structo-0.0.10/structo/serializers/string.py +27 -0
  39. {structo-0.0.7 → structo-0.0.10}/tests/test_array.py +2 -2
  40. {structo-0.0.7 → structo-0.0.10}/tests/test_list.py +1 -1
  41. structo-0.0.10/tests/test_literal.py +13 -0
  42. {structo-0.0.7 → structo-0.0.10}/tests/utils.py +0 -9
  43. structo-0.0.7/PKG-INFO +0 -8
  44. structo-0.0.7/examples/custom_serializer.py +0 -52
  45. structo-0.0.7/pyproject.toml +0 -19
  46. structo-0.0.7/structo/object.py +0 -58
  47. structo-0.0.7/structo/packed.py +0 -78
  48. structo-0.0.7/structo/serializer.py +0 -26
  49. structo-0.0.7/structo/types/array.py +0 -34
  50. structo-0.0.7/structo/types/blob.py +0 -19
  51. structo-0.0.7/structo/types/buffer.py +0 -21
  52. structo-0.0.7/structo/types/list.py +0 -32
  53. structo-0.0.7/structo/types/string.py +0 -21
  54. structo-0.0.7/structo/utils.py +0 -95
  55. {structo-0.0.7 → structo-0.0.10}/.gitignore +0 -0
  56. {structo-0.0.7 → structo-0.0.10}/LICENSE +0 -0
  57. {structo-0.0.7 → structo-0.0.10}/README.md +0 -0
  58. {structo-0.0.7 → structo-0.0.10}/pytest.ini +0 -0
  59. {structo-0.0.7 → structo-0.0.10}/tests/test_object.py +0 -0
  60. {structo-0.0.7 → structo-0.0.10}/tests/test_packed.py +0 -0
  61. {structo-0.0.7 → structo-0.0.10}/tests/test_primatives.py +0 -0
  62. {structo-0.0.7 → structo-0.0.10}/tests/test_size.py +0 -0
  63. {structo-0.0.7 → structo-0.0.10}/uv.lock +0 -0
@@ -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
- with open("example.wav", "rb") as f:
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'fmt '
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
- print(format)
39
- print(len(wav_data))
44
+ ```
@@ -0,0 +1,14 @@
1
+ # Structo
2
+
3
+ Basic
4
+
5
+ ## Commands
6
+
7
+ * Foo
8
+
9
+ ## Project layout
10
+
11
+ mkdocs.yml # The configuration file.
12
+ docs/
13
+ index.md # The documentation homepage.
14
+ ... # Other markdown pages, images and other files.
@@ -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,4 @@
1
+ # Seucrit
2
+
3
+ Basic
4
+
@@ -0,0 +1,3 @@
1
+ # Array
2
+
3
+ TODO
@@ -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
+ # List
2
+
3
+ A dynaimcally length list of values, prefixed with a length.
4
+
5
+ You must specifiy the length type and value type
6
+
7
+ ## Examples
8
+
9
+ ```py
10
+ List(String())
11
+ # list[float] with max length of 65536
12
+ List(float32, length=uint16_LE)
13
+ ```
@@ -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,8 @@
1
+ # Serializable Object
2
+
3
+ TODO
4
+ ## Usage
5
+
6
+ ## Helper Methods
7
+
8
+
@@ -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, buf, value):
24
+ def write(self, f, value):
25
25
  for item in value:
26
- buf.write(CONTINUE_BYTE)
27
- item.write(buf)
26
+ f.write(CONTINUE_BYTE)
27
+ item.write(f)
28
28
 
29
- buf.write(NULL_TERMINATOR)
29
+ f.write(NULL_TERMINATOR)
30
30
 
31
- def read(self, buf):
31
+ def read(self, f):
32
32
  while True:
33
- continue_byte = buf.read(1)
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(buf)
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.7"
5
+ __version__ = "0.0.10"
6
6
 
7
- from .serializer import Serializer, Serializable
7
+ from .interfaces import Serializer, Serializable
8
8
  from .serialise import get_serializer
9
- from .object import SerializableObject
10
- from .packed import PackedInts, PackedInt
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
- ObjectSerializer,
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)
@@ -0,0 +1,2 @@
1
+ from .packed import PackedInt, PackedInts
2
+ from .serializable_object import SerializableObject