pyccobs 0.1.1__py3-none-win_arm64.whl
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.
- ccobs/__init__.py +312 -0
- ccobs/_lib.py +131 -0
- ccobs/ccobs.dll +0 -0
- pyccobs-0.1.1.dist-info/METADATA +71 -0
- pyccobs-0.1.1.dist-info/RECORD +9 -0
- pyccobs-0.1.1.dist-info/WHEEL +5 -0
- pyccobs-0.1.1.dist-info/licenses/LICENSE +15 -0
- pyccobs-0.1.1.dist-info/licenses/vendor/ccobs/LICENSE +12 -0
- pyccobs-0.1.1.dist-info/top_level.txt +1 -0
ccobs/__init__.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Pythonic ctypes bindings for the ccobs C library."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from ._lib import BytePointer, StatePointer, bind
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Decoder",
|
|
13
|
+
"Encoder",
|
|
14
|
+
"Error",
|
|
15
|
+
"MalformedInputError",
|
|
16
|
+
"CcobsError",
|
|
17
|
+
"decode",
|
|
18
|
+
"encode",
|
|
19
|
+
"encodev",
|
|
20
|
+
"load_library",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Error(IntEnum):
|
|
25
|
+
OK = 0
|
|
26
|
+
PARTIAL = 1
|
|
27
|
+
NEED_OUTPUT = 2
|
|
28
|
+
NULL_ARGUMENT = 3
|
|
29
|
+
MALFORMED_INPUT = 4
|
|
30
|
+
OOM = 5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CcobsError(Exception):
|
|
34
|
+
"""An error reported by the native ccobs library."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, code: int, message: str):
|
|
37
|
+
self.code = Error(code) if code in Error._value2member_map_ else code
|
|
38
|
+
super().__init__(message)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MalformedInputError(CcobsError):
|
|
42
|
+
"""The encoded stream contains a malformed COBS frame."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_library: Optional[ctypes.CDLL] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_library(path: Optional[os.PathLike[str] | str] = None) -> ctypes.CDLL:
|
|
49
|
+
"""Load and return ccobs, optionally from an explicit path.
|
|
50
|
+
|
|
51
|
+
Calling this with a path replaces the process-wide library used by new
|
|
52
|
+
encoders and decoders. Existing objects retain their original library.
|
|
53
|
+
"""
|
|
54
|
+
global _library
|
|
55
|
+
if path is not None or _library is None:
|
|
56
|
+
_library = bind(path)
|
|
57
|
+
return _library
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _message(lib: ctypes.CDLL, code: int) -> str:
|
|
61
|
+
value = lib.ccobs_error_string(code)
|
|
62
|
+
return value.decode("utf-8", "replace") if value else f"ccobs error {code}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _raise_error(lib: ctypes.CDLL, code: int) -> None:
|
|
66
|
+
"""Wrapper to turn ccobs error into idiomatic Python errors."""
|
|
67
|
+
cls = MalformedInputError if code == Error.MALFORMED_INPUT else CcobsError
|
|
68
|
+
raise cls(code, _message(lib, code))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _input_buffer(data: bytes) -> tuple[object | None, BytePointer]:
|
|
72
|
+
if not data:
|
|
73
|
+
return None, BytePointer()
|
|
74
|
+
owner = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
|
|
75
|
+
return owner, ctypes.cast(owner, BytePointer)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _State:
|
|
79
|
+
"""Wrapper to internal ccobs state."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, library: Optional[ctypes.CDLL] = None):
|
|
82
|
+
self._lib = library or load_library()
|
|
83
|
+
error = ctypes.c_int(Error.OK)
|
|
84
|
+
self._state: StatePointer = self._lib.ccobs_create(ctypes.byref(error))
|
|
85
|
+
if not self._state:
|
|
86
|
+
_raise_error(self._lib, error.value)
|
|
87
|
+
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
state = getattr(self, "_state", None)
|
|
90
|
+
if state:
|
|
91
|
+
self._lib.ccobs_destroy(state)
|
|
92
|
+
self._state = StatePointer()
|
|
93
|
+
|
|
94
|
+
def reset(self) -> None:
|
|
95
|
+
code = self._lib.ccobs_reset(self._state)
|
|
96
|
+
if code != Error.OK:
|
|
97
|
+
_raise_error(self._lib, code)
|
|
98
|
+
|
|
99
|
+
def __enter__(self):
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def __exit__(self, *args) -> None:
|
|
103
|
+
self.close()
|
|
104
|
+
|
|
105
|
+
def __del__(self):
|
|
106
|
+
self.close()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Encoder(_State):
|
|
110
|
+
"""Incrementally encode message chunks or batches of complete messages."""
|
|
111
|
+
|
|
112
|
+
_OUTPUT_CHUNK_SIZE = 64 * 1024
|
|
113
|
+
|
|
114
|
+
def feed(self, data: object = b"", *, end_message: bool = False) -> bytes:
|
|
115
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
116
|
+
if end_message:
|
|
117
|
+
return self.feedv((raw,))
|
|
118
|
+
|
|
119
|
+
owner, _ = _input_buffer(raw)
|
|
120
|
+
offset = 0
|
|
121
|
+
result = bytearray()
|
|
122
|
+
# 257 also drains a full native block retained from an earlier call.
|
|
123
|
+
capacity = max(257, int(self._lib.ccobs_encode_max_size(len(raw))))
|
|
124
|
+
while True:
|
|
125
|
+
output = (ctypes.c_uint8 * capacity)()
|
|
126
|
+
available = ctypes.c_size_t(len(raw) - offset)
|
|
127
|
+
size = ctypes.c_size_t(capacity)
|
|
128
|
+
current = (
|
|
129
|
+
ctypes.cast(ctypes.byref(owner, offset), BytePointer)
|
|
130
|
+
if owner
|
|
131
|
+
else BytePointer()
|
|
132
|
+
)
|
|
133
|
+
code = self._lib.ccobs_encode(
|
|
134
|
+
self._state,
|
|
135
|
+
current,
|
|
136
|
+
ctypes.byref(available),
|
|
137
|
+
output,
|
|
138
|
+
ctypes.byref(size),
|
|
139
|
+
end_message,
|
|
140
|
+
)
|
|
141
|
+
offset += available.value
|
|
142
|
+
result.extend(output[: size.value])
|
|
143
|
+
|
|
144
|
+
if code == Error.NEED_OUTPUT:
|
|
145
|
+
continue
|
|
146
|
+
if code not in (Error.OK, Error.PARTIAL):
|
|
147
|
+
_raise_error(self._lib, code)
|
|
148
|
+
|
|
149
|
+
return bytes(result)
|
|
150
|
+
|
|
151
|
+
def feedv(self, messages: Iterable[object]) -> bytes:
|
|
152
|
+
"""Encode complete, independent messages into one flat framed stream.
|
|
153
|
+
|
|
154
|
+
The encoder state is retained between calls. If :meth:`feed` previously
|
|
155
|
+
supplied an unfinished message, the first message completes it.
|
|
156
|
+
"""
|
|
157
|
+
pending = [memoryview(message).cast("B").tobytes() for message in messages]
|
|
158
|
+
if not pending:
|
|
159
|
+
return b""
|
|
160
|
+
|
|
161
|
+
offsets = [0] * len(pending)
|
|
162
|
+
result = bytearray()
|
|
163
|
+
while pending:
|
|
164
|
+
count = len(pending)
|
|
165
|
+
owners: list[object | None] = []
|
|
166
|
+
pointers = (BytePointer * count)()
|
|
167
|
+
sizes = (ctypes.c_size_t * count)()
|
|
168
|
+
maximum = 0
|
|
169
|
+
for index, raw in enumerate(pending):
|
|
170
|
+
owner, pointer = _input_buffer(raw)
|
|
171
|
+
owners.append(owner)
|
|
172
|
+
offset = offsets[index]
|
|
173
|
+
pointers[index] = (
|
|
174
|
+
ctypes.cast(ctypes.byref(owner, offset), BytePointer)
|
|
175
|
+
if owner
|
|
176
|
+
else pointer
|
|
177
|
+
)
|
|
178
|
+
sizes[index] = len(raw) - offset
|
|
179
|
+
maximum += int(self._lib.ccobs_encode_max_size(sizes[index]))
|
|
180
|
+
|
|
181
|
+
capacity = max(257, min(maximum, self._OUTPUT_CHUNK_SIZE))
|
|
182
|
+
output = (ctypes.c_uint8 * capacity)()
|
|
183
|
+
completed = ctypes.c_size_t(count)
|
|
184
|
+
written = ctypes.c_size_t(capacity)
|
|
185
|
+
code = self._lib.ccobs_encodev(
|
|
186
|
+
self._state,
|
|
187
|
+
pointers,
|
|
188
|
+
sizes,
|
|
189
|
+
ctypes.byref(completed),
|
|
190
|
+
output,
|
|
191
|
+
ctypes.byref(written),
|
|
192
|
+
)
|
|
193
|
+
result.extend(output[: written.value])
|
|
194
|
+
|
|
195
|
+
for index in range(min(completed.value + 1, count)):
|
|
196
|
+
offsets[index] += sizes[index]
|
|
197
|
+
if completed.value:
|
|
198
|
+
del pending[: completed.value]
|
|
199
|
+
del offsets[: completed.value]
|
|
200
|
+
|
|
201
|
+
if code == Error.NEED_OUTPUT:
|
|
202
|
+
continue
|
|
203
|
+
if code != Error.OK:
|
|
204
|
+
_raise_error(self._lib, code)
|
|
205
|
+
if completed.value != count:
|
|
206
|
+
raise CcobsError(
|
|
207
|
+
Error.PARTIAL, "ccobs_encodev did not complete the batch"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return bytes(result)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class Decoder(_State):
|
|
214
|
+
"""Incrementally decode framed stream chunks."""
|
|
215
|
+
|
|
216
|
+
def feed(self, data: object = b"") -> tuple[bytes, int, bool]:
|
|
217
|
+
"""Decode at most one frame and report whether it was completed."""
|
|
218
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
219
|
+
owner, _ = _input_buffer(raw)
|
|
220
|
+
offset = 0
|
|
221
|
+
result = bytearray()
|
|
222
|
+
complete = ctypes.c_bool(False)
|
|
223
|
+
capacity = max(1, len(raw))
|
|
224
|
+
while True:
|
|
225
|
+
output = (ctypes.c_uint8 * capacity)()
|
|
226
|
+
available = ctypes.c_size_t(len(raw) - offset)
|
|
227
|
+
size = ctypes.c_size_t(capacity)
|
|
228
|
+
current = (
|
|
229
|
+
ctypes.cast(ctypes.byref(owner, offset), BytePointer)
|
|
230
|
+
if owner
|
|
231
|
+
else BytePointer()
|
|
232
|
+
)
|
|
233
|
+
code = self._lib.ccobs_decode(
|
|
234
|
+
self._state,
|
|
235
|
+
current,
|
|
236
|
+
ctypes.byref(available),
|
|
237
|
+
output,
|
|
238
|
+
ctypes.byref(size),
|
|
239
|
+
ctypes.byref(complete),
|
|
240
|
+
)
|
|
241
|
+
offset += available.value
|
|
242
|
+
result.extend(output[: size.value])
|
|
243
|
+
if code == Error.NEED_OUTPUT:
|
|
244
|
+
continue
|
|
245
|
+
if code not in (Error.OK, Error.PARTIAL):
|
|
246
|
+
_raise_error(self._lib, code)
|
|
247
|
+
|
|
248
|
+
return bytes(result), offset, complete.value
|
|
249
|
+
|
|
250
|
+
def feedv(self, data: object = b"") -> tuple[bytes, int, tuple[int, ...]]:
|
|
251
|
+
"""Decode framed stream data into one flat byte string.
|
|
252
|
+
|
|
253
|
+
The decoder state is retained between calls. The returned tuple contains
|
|
254
|
+
the decoded bytes, consumed input byte count, and completed frame sizes.
|
|
255
|
+
If a frame began in a previous call, its size includes bytes returned by
|
|
256
|
+
that call.
|
|
257
|
+
"""
|
|
258
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
259
|
+
owner, _ = _input_buffer(raw)
|
|
260
|
+
output_capacity = max(1, len(raw))
|
|
261
|
+
output = (ctypes.c_uint8 * output_capacity)()
|
|
262
|
+
available = ctypes.c_size_t(len(raw))
|
|
263
|
+
written = ctypes.c_size_t(output_capacity)
|
|
264
|
+
|
|
265
|
+
# Every complete encoded frame ends in zero. The extra slot lets
|
|
266
|
+
# decodev consume a partial frame following the final delimiter.
|
|
267
|
+
frame_capacity = raw.count(0) + 1
|
|
268
|
+
frame_sizes = (ctypes.c_size_t * frame_capacity)()
|
|
269
|
+
frame_count = ctypes.c_size_t(frame_capacity)
|
|
270
|
+
code = self._lib.ccobs_decodev(
|
|
271
|
+
self._state,
|
|
272
|
+
ctypes.cast(owner, BytePointer) if owner else BytePointer(),
|
|
273
|
+
ctypes.byref(available),
|
|
274
|
+
output,
|
|
275
|
+
ctypes.byref(written),
|
|
276
|
+
frame_sizes,
|
|
277
|
+
ctypes.byref(frame_count),
|
|
278
|
+
)
|
|
279
|
+
if code not in (Error.OK, Error.PARTIAL):
|
|
280
|
+
_raise_error(self._lib, code)
|
|
281
|
+
|
|
282
|
+
sizes = tuple(frame_sizes[: frame_count.value])
|
|
283
|
+
return bytes(output[: written.value]), available.value, sizes
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def encode(data: object, *, library: Optional[ctypes.CDLL] = None) -> bytes:
|
|
287
|
+
"""Encode one bytes-like message, including its trailing zero delimiter."""
|
|
288
|
+
with Encoder(library) as encoder:
|
|
289
|
+
return encoder.feed(data, end_message=True)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def encodev(
|
|
293
|
+
messages: Iterable[object], *, library: Optional[ctypes.CDLL] = None
|
|
294
|
+
) -> bytes:
|
|
295
|
+
"""Encode a batch of bytes-like messages into one flat framed stream."""
|
|
296
|
+
with Encoder(library) as encoder:
|
|
297
|
+
return encoder.feedv(messages)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def decode(data: object, *, library: Optional[ctypes.CDLL] = None) -> bytes:
|
|
301
|
+
"""Decode exactly one complete, zero-delimited frame."""
|
|
302
|
+
raw = memoryview(data).cast("B").tobytes()
|
|
303
|
+
with Decoder(library) as decoder:
|
|
304
|
+
output, consumed, complete = decoder.feed(raw)
|
|
305
|
+
|
|
306
|
+
if not complete:
|
|
307
|
+
raise ValueError("incomplete COBS frame")
|
|
308
|
+
|
|
309
|
+
if consumed != len(raw):
|
|
310
|
+
raise ValueError("input contains data after the first COBS frame")
|
|
311
|
+
|
|
312
|
+
return output
|
ccobs/_lib.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""ctypes declarations and shared-library discovery for ccobs."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import ctypes.util
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class State(ctypes.Structure):
|
|
12
|
+
"""Opaque ``ccobs_state``; allocate it through ``ccobs_create``."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Opaque state pointer to ccobs
|
|
16
|
+
StatePointer = ctypes.POINTER(State)
|
|
17
|
+
|
|
18
|
+
# Miscellaneous pointer types
|
|
19
|
+
BytePointer = ctypes.POINTER(ctypes.c_uint8)
|
|
20
|
+
SizePointer = ctypes.POINTER(ctypes.c_size_t)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _library_names() -> tuple[str, ...]:
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
return ("ccobs.dll", "libccobs.dll")
|
|
26
|
+
if sys.platform == "darwin":
|
|
27
|
+
return ("libccobs.dylib", "ccobs.dylib")
|
|
28
|
+
return ("libccobs.so",)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _bundled_library_paths(package_dir: Path) -> list[str]:
|
|
32
|
+
"""Return platform library names produced by the CMake build."""
|
|
33
|
+
if sys.platform == "win32":
|
|
34
|
+
patterns = ("ccobs.dll",)
|
|
35
|
+
elif sys.platform == "darwin":
|
|
36
|
+
patterns = ("libccobs.dylib",)
|
|
37
|
+
else:
|
|
38
|
+
patterns = ("libccobs.so",)
|
|
39
|
+
|
|
40
|
+
return [str(path) for pattern in patterns for path in package_dir.glob(pattern)]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_library(path: Optional[os.PathLike[str] | str] = None) -> ctypes.CDLL:
|
|
44
|
+
candidates: list[str] = []
|
|
45
|
+
explicit = path if path is not None else os.environ.get("CCOBS_LIBRARY_PATH")
|
|
46
|
+
if explicit:
|
|
47
|
+
explicit_path = Path(explicit).expanduser()
|
|
48
|
+
if explicit_path.is_dir():
|
|
49
|
+
candidates.extend(str(explicit_path / name) for name in _library_names())
|
|
50
|
+
else:
|
|
51
|
+
candidates.append(str(explicit_path))
|
|
52
|
+
|
|
53
|
+
package_dir = Path(__file__).resolve().parent
|
|
54
|
+
candidates.extend(_bundled_library_paths(package_dir))
|
|
55
|
+
candidates.extend(str(package_dir / name) for name in _library_names())
|
|
56
|
+
discovered = ctypes.util.find_library("ccobs")
|
|
57
|
+
if discovered:
|
|
58
|
+
candidates.append(discovered)
|
|
59
|
+
|
|
60
|
+
errors: list[str] = []
|
|
61
|
+
for candidate in candidates:
|
|
62
|
+
try:
|
|
63
|
+
return ctypes.CDLL(candidate)
|
|
64
|
+
except OSError as exc:
|
|
65
|
+
errors.append(f"{candidate}: {exc}")
|
|
66
|
+
|
|
67
|
+
detail = "\n".join(errors) if errors else "no library candidates were found"
|
|
68
|
+
raise OSError(
|
|
69
|
+
"Unable to load the ccobs shared library. Install it where the platform "
|
|
70
|
+
"loader can find it, or set CCOBS_LIBRARY_PATH to the library file or "
|
|
71
|
+
f"its directory. Attempts:\n{detail}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def bind(path: Optional[os.PathLike[str] | str] = None) -> ctypes.CDLL:
|
|
76
|
+
"""Load ccobs and apply exact argument/result declarations to its C API."""
|
|
77
|
+
|
|
78
|
+
lib = _load_library(path)
|
|
79
|
+
|
|
80
|
+
# ccobs_create
|
|
81
|
+
lib.ccobs_create.argtypes = [ctypes.POINTER(ctypes.c_int)]
|
|
82
|
+
lib.ccobs_create.restype = StatePointer
|
|
83
|
+
|
|
84
|
+
# ccobs_init
|
|
85
|
+
lib.ccobs_init.argtypes = [StatePointer]
|
|
86
|
+
lib.ccobs_init.restype = ctypes.c_int
|
|
87
|
+
|
|
88
|
+
# ccobs_reset
|
|
89
|
+
lib.ccobs_reset.argtypes = [StatePointer]
|
|
90
|
+
lib.ccobs_reset.restype = ctypes.c_int
|
|
91
|
+
|
|
92
|
+
# ccobs_destroy
|
|
93
|
+
lib.ccobs_destroy.argtypes = [StatePointer]
|
|
94
|
+
lib.ccobs_destroy.restype = None
|
|
95
|
+
|
|
96
|
+
# ccobs_encode
|
|
97
|
+
lib.ccobs_encode.argtypes = [
|
|
98
|
+
StatePointer, BytePointer, SizePointer, BytePointer, SizePointer, ctypes.c_bool
|
|
99
|
+
]
|
|
100
|
+
lib.ccobs_encode.restype = ctypes.c_int
|
|
101
|
+
|
|
102
|
+
# ccobs_decode
|
|
103
|
+
lib.ccobs_decode.argtypes = [
|
|
104
|
+
StatePointer, BytePointer, SizePointer, BytePointer, SizePointer,
|
|
105
|
+
ctypes.POINTER(ctypes.c_bool),
|
|
106
|
+
]
|
|
107
|
+
lib.ccobs_decode.restype = ctypes.c_int
|
|
108
|
+
|
|
109
|
+
# ccobs_encode_max_size
|
|
110
|
+
lib.ccobs_encode_max_size.argtypes = [ctypes.c_size_t]
|
|
111
|
+
lib.ccobs_encode_max_size.restype = ctypes.c_size_t
|
|
112
|
+
|
|
113
|
+
# ccobs_encodev
|
|
114
|
+
lib.ccobs_encodev.argtypes = [
|
|
115
|
+
StatePointer, ctypes.POINTER(BytePointer), SizePointer, SizePointer,
|
|
116
|
+
BytePointer, SizePointer,
|
|
117
|
+
]
|
|
118
|
+
lib.ccobs_encodev.restype = ctypes.c_int
|
|
119
|
+
|
|
120
|
+
# ccobs_decodev
|
|
121
|
+
lib.ccobs_decodev.argtypes = [
|
|
122
|
+
StatePointer, BytePointer, SizePointer, BytePointer, SizePointer,
|
|
123
|
+
SizePointer, SizePointer,
|
|
124
|
+
]
|
|
125
|
+
lib.ccobs_decodev.restype = ctypes.c_int
|
|
126
|
+
|
|
127
|
+
# ccobs_error_string
|
|
128
|
+
lib.ccobs_error_string.argtypes = [ctypes.c_int]
|
|
129
|
+
lib.ccobs_error_string.restype = ctypes.c_char_p
|
|
130
|
+
|
|
131
|
+
return lib
|
ccobs/ccobs.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyccobs
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Portable ctypes bindings for ccobs
|
|
5
|
+
Author: Elizabeth Kiara Regina Ashford
|
|
6
|
+
License-Expression: 0BSD
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: C
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: vendor/ccobs/LICENSE
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# pyccobs
|
|
18
|
+
|
|
19
|
+
`pyccobs` provides Python bindings to [ccobs](https://github.com/Elizafox/ccobs)
|
|
20
|
+
using only the standard library's `ctypes` module. The ccobs C library is
|
|
21
|
+
vendored and built into the package, so a separate ccobs installation is not
|
|
22
|
+
required. It includes one-shot and streaming APIs as well as exposing the
|
|
23
|
+
native library handle.
|
|
24
|
+
|
|
25
|
+
## Build and use
|
|
26
|
+
|
|
27
|
+
Python 3.11+ and CMake 3.15+ are required.
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
python -m pip install -e .
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The package build invokes the vendored ccobs CMake project directly. Because
|
|
34
|
+
the package uses `ctypes` rather than the CPython extension API, the resulting
|
|
35
|
+
wheel is tagged for Python 3 generally (for example, `py3-none-win_amd64`).
|
|
36
|
+
|
|
37
|
+
Set `CCOBS_LIBRARY_PATH` to a compatible shared library (or its directory) to
|
|
38
|
+
override the bundled copy when testing a different ccobs build.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import ccobs
|
|
42
|
+
|
|
43
|
+
wire = ccobs.encode(b"hello\0world")
|
|
44
|
+
assert ccobs.decode(wire) == b"hello\0world"
|
|
45
|
+
|
|
46
|
+
batch = ccobs.encodev([b"first", b"", b"third"])
|
|
47
|
+
assert batch == b"".join(ccobs.encode(item) for item in [b"first", b"", b"third"])
|
|
48
|
+
|
|
49
|
+
encoder = ccobs.Encoder()
|
|
50
|
+
wire = encoder.feed(b"hello ") + encoder.feed(b"world", end_message=True)
|
|
51
|
+
wire += encoder.feedv([b"second", b"third"])
|
|
52
|
+
encoder.close()
|
|
53
|
+
|
|
54
|
+
decoder = ccobs.Decoder()
|
|
55
|
+
decoded, used, frame_sizes = decoder.feedv(wire)
|
|
56
|
+
decoder.close()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`Decoder.feed()` returns `(decoded_bytes, consumed_bytes, message_complete)`
|
|
60
|
+
and stops after the first complete frame. `Decoder.feedv()` returns
|
|
61
|
+
`(decoded_bytes, consumed_bytes, frame_sizes)` and
|
|
62
|
+
decodes every complete frame in the supplied chunk in one `ccobs_decodev()`
|
|
63
|
+
call. `frame_sizes` gives the decoded size of each completed frame in the flat
|
|
64
|
+
output; a size may include bytes returned by an earlier partial call.
|
|
65
|
+
`Encoder.feedv()` encodes a batch of complete messages through the native
|
|
66
|
+
streaming vector API. Both stateful classes are context managers and have
|
|
67
|
+
`reset()` and `close()`.
|
|
68
|
+
|
|
69
|
+
For direct access to `ccobs_create`, `ccobs_encodev`, and the rest of the C API,
|
|
70
|
+
use `ccobs.load_library()`. Its functions have their ctypes `argtypes` and
|
|
71
|
+
`restype` declarations installed.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ccobs/__init__.py,sha256=0fUMmrmPt3TQeR0LS537qWcxR-XjzrNaX1NPn6U5LFg,10749
|
|
2
|
+
ccobs/_lib.py,sha256=pzcQj39QUba2YvYmdxx0rMejA08SfAZfp8YzbSxroR0,4256
|
|
3
|
+
ccobs/ccobs.dll,sha256=PMQnPayCVIxWPqbc0Ue72mqyIYfElxxT1wp4_Ai08ws,13312
|
|
4
|
+
pyccobs-0.1.1.dist-info/licenses/LICENSE,sha256=XHm6een4gLbCb5HjVD1_gJfGuWhxrwTZYPm87gULJYM,691
|
|
5
|
+
pyccobs-0.1.1.dist-info/licenses/vendor/ccobs/LICENSE,sha256=ERvxRExI2KNtyhDgYvhgfG3fp5bUhMqD0Z0W8tKwZVs,670
|
|
6
|
+
pyccobs-0.1.1.dist-info/METADATA,sha256=EDWKBsNQoLa4M26qh_yrSIZD2ukdj-fpg-1_S5l07R4,2623
|
|
7
|
+
pyccobs-0.1.1.dist-info/WHEEL,sha256=MeHYnn3dJDyOdmwbmKeeTp6EsuI4RKEVPLYFUebszcw,98
|
|
8
|
+
pyccobs-0.1.1.dist-info/top_level.txt,sha256=MfgbKG2TnFepR57Kt4EmrpApp6FyCIxB7k_scVqIaIU,6
|
|
9
|
+
pyccobs-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Zero-Clause BSD
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2026 Elizabeth Kiara Regina Ashford
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
15
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (C) 2026 Elizabeth Kiara Regina Ashford
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
4
|
+
purpose with or without fee is hereby granted.
|
|
5
|
+
|
|
6
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
7
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
8
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
9
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
10
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
11
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
12
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ccobs
|