packbin 0.1.9__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.
packbin-0.1.9/PKG-INFO ADDED
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: packbin
3
+ Version: 0.1.9
4
+ Summary: Pack and unpack a caller-owned field list
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+
9
+ # packbin
10
+ Binary packing and unpacking across languages, declarative mapping, and zero overhead in the binary data.
11
+
12
+ Both sides keep the same field list. The bytes are only the values.
13
+
14
+ Can be used for WebSocket, TCP, UDP, and other means of efficient communication
15
+
16
+ ## Example
17
+
18
+ Python → binary → TypeScript
19
+
20
+ ### Python
21
+
22
+ ```python
23
+ from packbin import BinaryPacker, Scheme, flags, i16, i32, u8, u16
24
+
25
+ class Position:
26
+ def __init__(self):
27
+ self.sid = 1
28
+ self.lat = 500_000_000
29
+ self.lon = 300_000_000
30
+ self.profile = 1
31
+ self.heading = None
32
+ self.speed = None
33
+ self.altitude = None
34
+
35
+ def bind(name):
36
+ return (lambda row: getattr(row, name), lambda row, value: setattr(row, name, value))
37
+
38
+ target = Scheme(
39
+ 0x40,
40
+ Position,
41
+ u16(0, *bind("sid")),
42
+ i32(1, *bind("lat")),
43
+ i32(2, *bind("lon")),
44
+ u8(3, *bind("profile")),
45
+ flags(
46
+ u16(4, *bind("heading")),
47
+ u8(5, *bind("speed")),
48
+ i16(6, *bind("altitude")),
49
+ ),
50
+ )
51
+
52
+ raw = BinaryPacker.pack(target, Position())
53
+ ```
54
+
55
+ ```
56
+ 40 01 00 00 65 cd 1d 00 a3 e1 11 01 00
57
+ ```
58
+
59
+ ### TypeScript
60
+
61
+ ```ts
62
+ import { BinaryPacker, flags, i16, i32, scheme, u8, u16 } from "packbin"
63
+
64
+ class Target {
65
+ sid = 1
66
+ lat = 500_000_000
67
+ lon = 300_000_000
68
+ profile = 1
69
+ heading: number | null = null
70
+ speed: number | null = null
71
+ altitude: number | null = null
72
+ }
73
+
74
+ const target = scheme<Target>(
75
+ 0x40,
76
+ u16(0, (x) => x.sid),
77
+ i32(1, (x) => x.lat),
78
+ i32(2, (x) => x.lon),
79
+ u8(3, (x) => x.profile),
80
+ flags([
81
+ u16(4, (x) => x.heading),
82
+ u8(5, (x) => x.speed),
83
+ i16(6, (x) => x.altitude),
84
+ ]),
85
+ )
86
+
87
+ const got = BinaryPacker.unpack(target, raw)
88
+ ```
89
+
90
+ `flags` is how an optional field takes no space when you have no value for it. `heading`, `speed`, and `altitude` are measurements, so `0` is still a value and has to be written. `None` means the field is not in the packet.
91
+
92
+ `motion` is always one byte in front of those fields. Bit 0 is `heading`, bit 1 is `speed`, bit 2 is `altitude`. A set bit writes that field next. A clear bit skips it.
93
+
94
+ | Field | Type | Bytes when present | Values |
95
+ |---|---|---|---|
96
+ | `heading` | `u16` | 2 | 0 … 65535 |
97
+ | `speed` | `u8` | 1 | 0 … 255 |
98
+ | `altitude` | `i16` | 2 | −32768 … 32767 |
99
+
100
+ In this example all three are `None`, so `motion` is `00` and those 5 bytes are absent. The packet is 13 bytes: type number 1, `sid` 2, `lat` 4, `lon` 4, `profile` 1, `motion` 1.
101
+
102
+ ```
103
+ 40 type
104
+ 01 00 sid
105
+ 00 65 cd 1d lat
106
+ 00 a3 e1 11 lon
107
+ 01 profile
108
+ 00 motion
109
+ ```
110
+
111
+ `heading = 90` sets bit 0, so `motion` is `01` and `5a 00` follows it. `heading = 0` sets the same bit and writes `00 00`. `speed = 10` sets bit 1 and writes one byte. The present fields are written in the order listed, and only those.
112
+
113
+ ## Example
114
+
115
+ C# → binary → Rust
116
+
117
+ ### C#
118
+
119
+ ```csharp
120
+ using Packbin;
121
+
122
+ sealed class User
123
+ {
124
+ public string Username { get; set; } = "";
125
+ public List<Role> Roles { get; set; } = [];
126
+ public Dictionary<string, ActionList> Access { get; set; } = [];
127
+ }
128
+
129
+ sealed class Role { public string RoleName { get; set; } = ""; }
130
+ sealed class ActionName { public string Action { get; set; } = ""; }
131
+ sealed class ActionList { public List<ActionName> Actions { get; set; } = []; }
132
+
133
+ var userScheme = new Scheme<User>(1, f => [
134
+ f.Utf8(0, x => x.Username),
135
+ f.List(x => x.Roles, r => r.Utf8(0, role => role.RoleName)),
136
+ f.Dict(x => x.Access, e => e.List(a => a.Actions, n => n.Utf8(0, action => action.Action)))]);
137
+
138
+ var raw = BinaryPacker.Pack(userScheme, new User
139
+ {
140
+ Username = "ada",
141
+ Roles = [new Role { RoleName = "user" }, new Role { RoleName = "admin" }],
142
+ Access = new()
143
+ {
144
+ ["map"] = new ActionList { Actions = [new ActionName { Action = "read" }, new ActionName { Action = "edit" }] },
145
+ ["store"] = new ActionList { Actions = [new ActionName { Action = "write" }] },
146
+ },
147
+ });
148
+ ```
149
+
150
+ ```
151
+ 01 03 00 61 64 61 02 00 04 00 75 73 65 72 05 00 61 64 6d 69 6e
152
+ 02 00 03 00 6d 61 70 02 00 04 00 72 65 61 64 04 00 65 64 69 74
153
+ 05 00 73 74 6f 72 65 01 00 05 00 77 72 69 74 65
154
+ ```
155
+
156
+ The first byte is the scheme type number.
157
+
158
+ ### Rust
159
+
160
+ ```rust
161
+ use packbin::{dict, list, unpack_map, utf8, MapScheme};
162
+
163
+ let user = MapScheme::new(
164
+ 1,
165
+ vec![
166
+ utf8("username"),
167
+ list("roles", utf8("role")),
168
+ dict("access", list("actions", utf8("action"))),
169
+ ],
170
+ );
171
+
172
+ let got = unpack_map(&user, &raw).unwrap();
173
+ ```
174
+
175
+ ## Data types
176
+
177
+ | Helper | What it writes |
178
+ |--------|----------------|
179
+ | `u8` `u16` `u32` `u64` | unsigned integer, little-endian |
180
+ | `i8` `i16` `i32` `i64` | signed integer, little-endian |
181
+ | `f32` `f64` | IEEE 754 float |
182
+ | `bytes(n)` | exactly `n` raw bytes |
183
+ | `be(field)` | that number, big-endian |
184
+ | `utf8` | UTF-8 string, `u16` length |
185
+ | `list` | `u16` count, then that many elements |
186
+ | `dict` | `u16` pair count; keys in unsigned byte order |
187
+ | `flags(name, fields)` | one `u8`; bit 0 is the first field; a clear bit omits that field |
188
+ | `when(eq(field, value), fields)` | the group only when an earlier field equals `value` |
189
+ | `repeat(fields)` | the group until the buffer ends |
190
+
191
+ [`_docs/01_solution/schema.md`](_docs/01_solution/schema.md)
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,187 @@
1
+ # packbin
2
+ Binary packing and unpacking across languages, declarative mapping, and zero overhead in the binary data.
3
+
4
+ Both sides keep the same field list. The bytes are only the values.
5
+
6
+ Can be used for WebSocket, TCP, UDP, and other means of efficient communication
7
+
8
+ ## Example
9
+
10
+ Python → binary → TypeScript
11
+
12
+ ### Python
13
+
14
+ ```python
15
+ from packbin import BinaryPacker, Scheme, flags, i16, i32, u8, u16
16
+
17
+ class Position:
18
+ def __init__(self):
19
+ self.sid = 1
20
+ self.lat = 500_000_000
21
+ self.lon = 300_000_000
22
+ self.profile = 1
23
+ self.heading = None
24
+ self.speed = None
25
+ self.altitude = None
26
+
27
+ def bind(name):
28
+ return (lambda row: getattr(row, name), lambda row, value: setattr(row, name, value))
29
+
30
+ target = Scheme(
31
+ 0x40,
32
+ Position,
33
+ u16(0, *bind("sid")),
34
+ i32(1, *bind("lat")),
35
+ i32(2, *bind("lon")),
36
+ u8(3, *bind("profile")),
37
+ flags(
38
+ u16(4, *bind("heading")),
39
+ u8(5, *bind("speed")),
40
+ i16(6, *bind("altitude")),
41
+ ),
42
+ )
43
+
44
+ raw = BinaryPacker.pack(target, Position())
45
+ ```
46
+
47
+ ```
48
+ 40 01 00 00 65 cd 1d 00 a3 e1 11 01 00
49
+ ```
50
+
51
+ ### TypeScript
52
+
53
+ ```ts
54
+ import { BinaryPacker, flags, i16, i32, scheme, u8, u16 } from "packbin"
55
+
56
+ class Target {
57
+ sid = 1
58
+ lat = 500_000_000
59
+ lon = 300_000_000
60
+ profile = 1
61
+ heading: number | null = null
62
+ speed: number | null = null
63
+ altitude: number | null = null
64
+ }
65
+
66
+ const target = scheme<Target>(
67
+ 0x40,
68
+ u16(0, (x) => x.sid),
69
+ i32(1, (x) => x.lat),
70
+ i32(2, (x) => x.lon),
71
+ u8(3, (x) => x.profile),
72
+ flags([
73
+ u16(4, (x) => x.heading),
74
+ u8(5, (x) => x.speed),
75
+ i16(6, (x) => x.altitude),
76
+ ]),
77
+ )
78
+
79
+ const got = BinaryPacker.unpack(target, raw)
80
+ ```
81
+
82
+ `flags` is how an optional field takes no space when you have no value for it. `heading`, `speed`, and `altitude` are measurements, so `0` is still a value and has to be written. `None` means the field is not in the packet.
83
+
84
+ `motion` is always one byte in front of those fields. Bit 0 is `heading`, bit 1 is `speed`, bit 2 is `altitude`. A set bit writes that field next. A clear bit skips it.
85
+
86
+ | Field | Type | Bytes when present | Values |
87
+ |---|---|---|---|
88
+ | `heading` | `u16` | 2 | 0 … 65535 |
89
+ | `speed` | `u8` | 1 | 0 … 255 |
90
+ | `altitude` | `i16` | 2 | −32768 … 32767 |
91
+
92
+ In this example all three are `None`, so `motion` is `00` and those 5 bytes are absent. The packet is 13 bytes: type number 1, `sid` 2, `lat` 4, `lon` 4, `profile` 1, `motion` 1.
93
+
94
+ ```
95
+ 40 type
96
+ 01 00 sid
97
+ 00 65 cd 1d lat
98
+ 00 a3 e1 11 lon
99
+ 01 profile
100
+ 00 motion
101
+ ```
102
+
103
+ `heading = 90` sets bit 0, so `motion` is `01` and `5a 00` follows it. `heading = 0` sets the same bit and writes `00 00`. `speed = 10` sets bit 1 and writes one byte. The present fields are written in the order listed, and only those.
104
+
105
+ ## Example
106
+
107
+ C# → binary → Rust
108
+
109
+ ### C#
110
+
111
+ ```csharp
112
+ using Packbin;
113
+
114
+ sealed class User
115
+ {
116
+ public string Username { get; set; } = "";
117
+ public List<Role> Roles { get; set; } = [];
118
+ public Dictionary<string, ActionList> Access { get; set; } = [];
119
+ }
120
+
121
+ sealed class Role { public string RoleName { get; set; } = ""; }
122
+ sealed class ActionName { public string Action { get; set; } = ""; }
123
+ sealed class ActionList { public List<ActionName> Actions { get; set; } = []; }
124
+
125
+ var userScheme = new Scheme<User>(1, f => [
126
+ f.Utf8(0, x => x.Username),
127
+ f.List(x => x.Roles, r => r.Utf8(0, role => role.RoleName)),
128
+ f.Dict(x => x.Access, e => e.List(a => a.Actions, n => n.Utf8(0, action => action.Action)))]);
129
+
130
+ var raw = BinaryPacker.Pack(userScheme, new User
131
+ {
132
+ Username = "ada",
133
+ Roles = [new Role { RoleName = "user" }, new Role { RoleName = "admin" }],
134
+ Access = new()
135
+ {
136
+ ["map"] = new ActionList { Actions = [new ActionName { Action = "read" }, new ActionName { Action = "edit" }] },
137
+ ["store"] = new ActionList { Actions = [new ActionName { Action = "write" }] },
138
+ },
139
+ });
140
+ ```
141
+
142
+ ```
143
+ 01 03 00 61 64 61 02 00 04 00 75 73 65 72 05 00 61 64 6d 69 6e
144
+ 02 00 03 00 6d 61 70 02 00 04 00 72 65 61 64 04 00 65 64 69 74
145
+ 05 00 73 74 6f 72 65 01 00 05 00 77 72 69 74 65
146
+ ```
147
+
148
+ The first byte is the scheme type number.
149
+
150
+ ### Rust
151
+
152
+ ```rust
153
+ use packbin::{dict, list, unpack_map, utf8, MapScheme};
154
+
155
+ let user = MapScheme::new(
156
+ 1,
157
+ vec![
158
+ utf8("username"),
159
+ list("roles", utf8("role")),
160
+ dict("access", list("actions", utf8("action"))),
161
+ ],
162
+ );
163
+
164
+ let got = unpack_map(&user, &raw).unwrap();
165
+ ```
166
+
167
+ ## Data types
168
+
169
+ | Helper | What it writes |
170
+ |--------|----------------|
171
+ | `u8` `u16` `u32` `u64` | unsigned integer, little-endian |
172
+ | `i8` `i16` `i32` `i64` | signed integer, little-endian |
173
+ | `f32` `f64` | IEEE 754 float |
174
+ | `bytes(n)` | exactly `n` raw bytes |
175
+ | `be(field)` | that number, big-endian |
176
+ | `utf8` | UTF-8 string, `u16` length |
177
+ | `list` | `u16` count, then that many elements |
178
+ | `dict` | `u16` pair count; keys in unsigned byte order |
179
+ | `flags(name, fields)` | one `u8`; bit 0 is the first field; a clear bit omits that field |
180
+ | `when(eq(field, value), fields)` | the group only when an earlier field equals `value` |
181
+ | `repeat(fields)` | the group until the buffer ends |
182
+
183
+ [`_docs/01_solution/schema.md`](_docs/01_solution/schema.md)
184
+
185
+ ## License
186
+
187
+ MIT
@@ -0,0 +1,18 @@
1
+ [project]
2
+ name = "packbin"
3
+ version = "0.1.9"
4
+ description = "Pack and unpack a caller-owned field list"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ requires-python = ">=3.10"
8
+ dependencies = []
9
+
10
+ [build-system]
11
+ requires = ["setuptools>=61"]
12
+ build-backend = "setuptools.build_meta"
13
+
14
+ [tool.setuptools.packages.find]
15
+ where = ["src"]
16
+
17
+ [tool.pytest.ini_options]
18
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from packbin._errors import ShortPacket, TrailingBytes, TypeMismatch, UnpackResult
4
+ from packbin._nodes import (
5
+ be,
6
+ bits,
7
+ bool,
8
+ bytes,
9
+ dict,
10
+ eq,
11
+ flags,
12
+ flag_byte,
13
+ group,
14
+ i8,
15
+ i16,
16
+ i32,
17
+ i64,
18
+ f32,
19
+ f64,
20
+ list,
21
+ repeat,
22
+ sized,
23
+ u2,
24
+ u8,
25
+ u16,
26
+ u32,
27
+ u64,
28
+ utf8,
29
+ when,
30
+ )
31
+ from packbin._scheme import BinaryPacker, Scheme
32
+
33
+ __all__ = [
34
+ "BinaryPacker",
35
+ "Scheme",
36
+ "ShortPacket",
37
+ "TrailingBytes",
38
+ "TypeMismatch",
39
+ "UnpackResult",
40
+ "u8",
41
+ "u16",
42
+ "u32",
43
+ "u64",
44
+ "i8",
45
+ "i16",
46
+ "i32",
47
+ "i64",
48
+ "f32",
49
+ "f64",
50
+ "bool",
51
+ "bytes",
52
+ "be",
53
+ "flags",
54
+ "flag_byte",
55
+ "eq",
56
+ "when",
57
+ "repeat",
58
+ "group",
59
+ "sized",
60
+ "u2",
61
+ "bits",
62
+ "utf8",
63
+ "list",
64
+ "dict",
65
+ ]
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Generic, TypeVar
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class ShortPacket:
11
+ field: str
12
+ needed: int
13
+ left: int
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class TrailingBytes:
18
+ left: int
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class TypeMismatch:
23
+ expected: int
24
+ actual: int
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class UnpackResult(Generic[T]):
29
+ ok: bool
30
+ value: T | None = None
31
+ error: ShortPacket | TrailingBytes | TypeMismatch | None = None
32
+
33
+ @property
34
+ def field(self) -> str | None:
35
+ if isinstance(self.error, ShortPacket):
36
+ return self.error.field
37
+ return None
38
+
39
+ @property
40
+ def needed(self) -> int | None:
41
+ if isinstance(self.error, ShortPacket):
42
+ return self.error.needed
43
+ return None
44
+
45
+ @property
46
+ def left(self) -> int | None:
47
+ if isinstance(self.error, (ShortPacket, TrailingBytes)):
48
+ return self.error.left
49
+ return None