socketwrapper 0.0.1__tar.gz → 0.0.2__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.
- {socketwrapper-0.0.1/socketwrapper.egg-info → socketwrapper-0.0.2}/PKG-INFO +91 -44
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/README.md +85 -41
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/pyproject.toml +5 -3
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper/__init__.py +35 -35
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper/_utils.py +73 -58
- socketwrapper-0.0.2/socketwrapper/framing.py +255 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper/protocols.py +3 -2
- {socketwrapper-0.0.1 → socketwrapper-0.0.2/socketwrapper.egg-info}/PKG-INFO +91 -44
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper.egg-info/requires.txt +5 -1
- socketwrapper-0.0.1/socketwrapper/framing.py +0 -143
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/LICENSE +0 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/setup.cfg +0 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper/_varint.py +0 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper/py.typed +0 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper.egg-info/SOURCES.txt +0 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper.egg-info/dependency_links.txt +0 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper.egg-info/top_level.txt +0 -0
- {socketwrapper-0.0.1 → socketwrapper-0.0.2}/socketwrapper.egg-info/zip-safe +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socketwrapper
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: high level socket and pipe
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: high level socket and pipe wrappers
|
|
5
5
|
Author: Felipe A Hernandez
|
|
6
6
|
Author-email: ergoithz@gmail.com
|
|
7
7
|
License: MIT License
|
|
@@ -41,9 +41,12 @@ Classifier: Topic :: System :: Networking
|
|
|
41
41
|
Requires-Python: >=3.12
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
|
-
Requires-Dist: typing-extensions>=4.
|
|
44
|
+
Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
|
|
45
|
+
Provides-Extra: msgpack
|
|
46
|
+
Requires-Dist: msgpack; extra == "msgpack"
|
|
45
47
|
Provides-Extra: dev
|
|
46
48
|
Requires-Dist: coverage; extra == "dev"
|
|
49
|
+
Requires-Dist: msgpack; extra == "dev"
|
|
47
50
|
Requires-Dist: ruff; extra == "dev"
|
|
48
51
|
Requires-Dist: wheel; extra == "dev"
|
|
49
52
|
Requires-Dist: yapf; extra == "dev"
|
|
@@ -52,24 +55,26 @@ Dynamic: license-file
|
|
|
52
55
|
# socketwrapper
|
|
53
56
|
|
|
54
57
|
This package provides high level wrappers for sockets and pipes:
|
|
55
|
-
- Thread-safe within both and between threads and asyncio realms.
|
|
56
|
-
- Managed sync recv and send operations with timeouts.
|
|
57
|
-
- Native asyncio recv_async and send_async operations.
|
|
58
|
-
- Pluggable message protocol (headered variable length data) supporting
|
|
59
|
-
header parsing, serialization and deserialization.
|
|
60
|
-
- Pluggable I/O protocol (file descriptor still required due asyncio).
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
- Thread-safe within both threads and asyncio realms, and between them.
|
|
60
|
+
- Managed sync `recv` and `send` operations with timeouts.
|
|
61
|
+
- Native asyncio `recv_async` and `send_async` operations.
|
|
62
|
+
- Pluggable message protocol (headered variable length data) supporting variable-length header parsing, payload serialization and deserialization.
|
|
63
|
+
- Pluggable I/O protocol (OS file descriptor still required).
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
all we need for IPC: header/payload logic and support for both sockets and pipes.
|
|
65
|
+
Builtin message protocols (framing):
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
- `socketwrapper.framing.VarIntBytes`: varint-headered bytes (default with `framing=True`).
|
|
68
|
+
- `socketwrapper.framing.MultiprocessingBytes` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
|
|
69
|
+
- `socketwrapper.framing.Multiprocessing` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
|
|
70
|
+
- `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data stream.
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
## Motivation
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
- There aren't a ton of high level asyncio socket wrappers out there providing all we need for IPC: header/payload message logic and support for both sockets and pipes.
|
|
75
|
+
- Most implementations got either hardcoded messaging protocols or require a fixed-size header.
|
|
76
|
+
- No implementation was thread-safe between both asyncio and threading realms.
|
|
77
|
+
- No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
|
|
73
78
|
|
|
74
79
|
## Installation
|
|
75
80
|
|
|
@@ -77,18 +82,28 @@ No other implementation directly supports wrapping multiprocessing Connections.
|
|
|
77
82
|
uv pip install socketwrapper
|
|
78
83
|
```
|
|
79
84
|
|
|
85
|
+
Or with optional [msgpack](https://pypi.org/project/msgpack/) support.
|
|
86
|
+
```sh
|
|
87
|
+
uv pip install 'socketwrapper[msgpack]'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Changelog
|
|
91
|
+
|
|
92
|
+
### v0.0.2 - 2025.11.05
|
|
93
|
+
|
|
94
|
+
- Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
|
|
95
|
+
- New: optional msgpack support (`msgpack` extra).
|
|
96
|
+
- Fix: busy reads no longer blocking asyncio event loop.
|
|
97
|
+
- Typing: expose `SizedBuffer` and deprecate `SendPayload`.
|
|
98
|
+
- Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
|
|
99
|
+
|
|
80
100
|
## Documentation
|
|
81
101
|
|
|
82
|
-
None other than this README, life's too short and I'm too busy with real life,
|
|
83
|
-
if you need better documentation consider donating to
|
|
84
|
-
[my ko-fi](https://ko-fi.com/s26me) stating that as the tip message,
|
|
85
|
-
check out how my docs look like at [mstache docs](https://mstache.readthedocs.io/en/latest/)
|
|
86
|
-
and [uactor docs](https://mstache.readthedocs.io/en/latest/).
|
|
102
|
+
None other than this README, life's too short and I'm too busy with real life, if you need better documentation consider donating to [my ko-fi](https://ko-fi.com/s26me) stating that as a tip message, check out how my docs look like at [mstache docs](https://mstache.readthedocs.io/en/latest/) and [uactor docs](https://mstache.readthedocs.io/en/latest/).
|
|
87
103
|
|
|
88
104
|
### Puggable I/O: SocketLike protocol
|
|
89
105
|
|
|
90
|
-
The `socketwrapper.SocketLike` protocol,
|
|
91
|
-
is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
106
|
+
The `socketwrapper.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
92
107
|
|
|
93
108
|
```py
|
|
94
109
|
@typing.runtime_checkable
|
|
@@ -103,15 +118,8 @@ class SocketLike(typing.Protocol):
|
|
|
103
118
|
```
|
|
104
119
|
|
|
105
120
|
Special attention to:
|
|
106
|
-
- [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno)
|
|
107
|
-
|
|
108
|
-
- [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout)
|
|
109
|
-
only requires support for `settimeout(.0)` (
|
|
110
|
-
[non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)
|
|
111
|
-
), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError)
|
|
112
|
-
for any other value is fully supported in which case
|
|
113
|
-
[selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector)
|
|
114
|
-
will be used for synchronous operations.
|
|
121
|
+
- [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
|
|
122
|
+
- [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout) must support `settimeout(.0)` ([non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError) for any other value will be handled, relying on [selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector) for synchronous operations.
|
|
115
123
|
|
|
116
124
|
## Usage
|
|
117
125
|
|
|
@@ -131,11 +139,31 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
|
|
|
131
139
|
else:
|
|
132
140
|
child_writer.send(b'Hello world!')
|
|
133
141
|
```
|
|
134
|
-
|
|
135
142
|
```
|
|
136
143
|
Message b'Hello world!' received
|
|
137
144
|
```
|
|
138
145
|
|
|
146
|
+
### Simple IPC with pipe using msgpack
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import os
|
|
150
|
+
import socketwrapper
|
|
151
|
+
import socketwrapper.framing as framing
|
|
152
|
+
|
|
153
|
+
with socketwrapper.pipe(framing=framing.MsgPack()) as (parent_reader, child_writer):
|
|
154
|
+
child_writer.inheritable = True
|
|
155
|
+
child_pid = os.fork() # replace with your own process fork/spawn logic
|
|
156
|
+
child_writer.inheritable = False # important, prevent socket leaks!
|
|
157
|
+
|
|
158
|
+
if child_pid:
|
|
159
|
+
print(f'Message {parent_reader.recv()!r} received')
|
|
160
|
+
else:
|
|
161
|
+
child_writer.send({'data': b'Hello world!'})
|
|
162
|
+
```
|
|
163
|
+
```
|
|
164
|
+
Message {'data': b'Hello world!'} received
|
|
165
|
+
```
|
|
166
|
+
|
|
139
167
|
### Simple asyncio IPC with pipe
|
|
140
168
|
|
|
141
169
|
```python
|
|
@@ -156,7 +184,6 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
|
|
|
156
184
|
|
|
157
185
|
asyncio.run(parent(parent_reader) if child_pid else child(child_writer))
|
|
158
186
|
```
|
|
159
|
-
|
|
160
187
|
```
|
|
161
188
|
Message b'Hello world!' received
|
|
162
189
|
```
|
|
@@ -180,7 +207,6 @@ with socketwrapper.socketpair(framing=True) as (parent_duplex, child_duplex):
|
|
|
180
207
|
print(f'Message {child_duplex.recv()!r} received in child')
|
|
181
208
|
child_duplex.send(b'Hello parent!')
|
|
182
209
|
```
|
|
183
|
-
|
|
184
210
|
```
|
|
185
211
|
Message b'Hello child!' received in child
|
|
186
212
|
Message b'Hello parent!' received in parent
|
|
@@ -204,25 +230,46 @@ def child(conn: multiprocessing.connection.Connection) -> None:
|
|
|
204
230
|
|
|
205
231
|
asyncio.run(main())
|
|
206
232
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
233
|
+
if __name__ == '__main__':
|
|
234
|
+
parent_conn, child_conn = multiprocessing.Pipe()
|
|
235
|
+
with parent_conn, child_conn:
|
|
236
|
+
child_process = multiprocessing.Process(target=child, args=(child_conn,))
|
|
237
|
+
child_process.start()
|
|
211
238
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
239
|
+
parent_conn.send_bytes(b'Hello child!')
|
|
240
|
+
print(f'Message {parent_conn.recv_bytes()!r} received in parent')
|
|
241
|
+
child_process.join(1)
|
|
215
242
|
```
|
|
216
|
-
|
|
217
243
|
```
|
|
218
244
|
Message b'Hello child!' received in child
|
|
219
245
|
Message b'Hello parent!' received in parent
|
|
220
246
|
```
|
|
221
247
|
|
|
248
|
+
### Socketwrapper for cross-interpreter communication
|
|
249
|
+
|
|
250
|
+
```py
|
|
251
|
+
import concurrent.futures
|
|
252
|
+
import socketwrapper
|
|
253
|
+
|
|
254
|
+
def child(child_fileno: int) -> None:
|
|
255
|
+
child_writer = socketwrapper.MessageWriter(child_fileno)
|
|
256
|
+
child_writer.send(b'Hello World')
|
|
257
|
+
|
|
258
|
+
if __name__ == '__main__':
|
|
259
|
+
with (socketwrapper.pipe(framing=True) as (parent_reader, child_writer),
|
|
260
|
+
concurrent.futures.InterpreterPoolExecutor() as pool):
|
|
261
|
+
pool.submit(child, child_writer.fileno())
|
|
262
|
+
print(f'Message {parent_reader.recv()!r} received')
|
|
263
|
+
```
|
|
264
|
+
```
|
|
265
|
+
Message b'Hello World' received
|
|
266
|
+
```
|
|
267
|
+
|
|
222
268
|
### Custom socketwrapper framing with progress
|
|
223
269
|
|
|
224
270
|
```py
|
|
225
271
|
import collections.abc
|
|
272
|
+
import io
|
|
226
273
|
import itertools
|
|
227
274
|
import os
|
|
228
275
|
import socketwrapper
|
|
@@ -246,7 +293,7 @@ def progress(arrow: str, size: int, min_chunk: int = 1024) -> collections.abc.Ge
|
|
|
246
293
|
class ProgressFraming(socketwrapper.framing.VarIntBytes):
|
|
247
294
|
|
|
248
295
|
@classmethod
|
|
249
|
-
def frames(cls, buffer:
|
|
296
|
+
def frames(cls, buffer: io.BytesIO) -> collections.abc.Generator[int, None, None]:
|
|
250
297
|
frames = super().frames(buffer)
|
|
251
298
|
yield from itertools.islice(frames, 2)
|
|
252
299
|
yield from progress('>', next(frames))
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
# socketwrapper
|
|
2
2
|
|
|
3
3
|
This package provides high level wrappers for sockets and pipes:
|
|
4
|
-
- Thread-safe within both and between threads and asyncio realms.
|
|
5
|
-
- Managed sync recv and send operations with timeouts.
|
|
6
|
-
- Native asyncio recv_async and send_async operations.
|
|
7
|
-
- Pluggable message protocol (headered variable length data) supporting
|
|
8
|
-
header parsing, serialization and deserialization.
|
|
9
|
-
- Pluggable I/O protocol (file descriptor still required due asyncio).
|
|
10
4
|
|
|
11
|
-
|
|
5
|
+
- Thread-safe within both threads and asyncio realms, and between them.
|
|
6
|
+
- Managed sync `recv` and `send` operations with timeouts.
|
|
7
|
+
- Native asyncio `recv_async` and `send_async` operations.
|
|
8
|
+
- Pluggable message protocol (headered variable length data) supporting variable-length header parsing, payload serialization and deserialization.
|
|
9
|
+
- Pluggable I/O protocol (OS file descriptor still required).
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
all we need for IPC: header/payload logic and support for both sockets and pipes.
|
|
11
|
+
Builtin message protocols (framing):
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
- `socketwrapper.framing.VarIntBytes`: varint-headered bytes (default with `framing=True`).
|
|
14
|
+
- `socketwrapper.framing.MultiprocessingBytes` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
|
|
15
|
+
- `socketwrapper.framing.Multiprocessing` (if platform is supported): [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
|
|
16
|
+
- `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data stream.
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
## Motivation
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
- There aren't a ton of high level asyncio socket wrappers out there providing all we need for IPC: header/payload message logic and support for both sockets and pipes.
|
|
21
|
+
- Most implementations got either hardcoded messaging protocols or require a fixed-size header.
|
|
22
|
+
- No implementation was thread-safe between both asyncio and threading realms.
|
|
23
|
+
- No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
@@ -26,18 +28,28 @@ No other implementation directly supports wrapping multiprocessing Connections.
|
|
|
26
28
|
uv pip install socketwrapper
|
|
27
29
|
```
|
|
28
30
|
|
|
31
|
+
Or with optional [msgpack](https://pypi.org/project/msgpack/) support.
|
|
32
|
+
```sh
|
|
33
|
+
uv pip install 'socketwrapper[msgpack]'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Changelog
|
|
37
|
+
|
|
38
|
+
### v0.0.2 - 2025.11.05
|
|
39
|
+
|
|
40
|
+
- Breaking: `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
|
|
41
|
+
- New: optional msgpack support (`msgpack` extra).
|
|
42
|
+
- Fix: busy reads no longer blocking asyncio event loop.
|
|
43
|
+
- Typing: expose `SizedBuffer` and deprecate `SendPayload`.
|
|
44
|
+
- Typing: `SocketWriter.send` and `SocketWriter.send_async` data is now `SizedBuffer`.
|
|
45
|
+
|
|
29
46
|
## Documentation
|
|
30
47
|
|
|
31
|
-
None other than this README, life's too short and I'm too busy with real life,
|
|
32
|
-
if you need better documentation consider donating to
|
|
33
|
-
[my ko-fi](https://ko-fi.com/s26me) stating that as the tip message,
|
|
34
|
-
check out how my docs look like at [mstache docs](https://mstache.readthedocs.io/en/latest/)
|
|
35
|
-
and [uactor docs](https://mstache.readthedocs.io/en/latest/).
|
|
48
|
+
None other than this README, life's too short and I'm too busy with real life, if you need better documentation consider donating to [my ko-fi](https://ko-fi.com/s26me) stating that as a tip message, check out how my docs look like at [mstache docs](https://mstache.readthedocs.io/en/latest/) and [uactor docs](https://mstache.readthedocs.io/en/latest/).
|
|
36
49
|
|
|
37
50
|
### Puggable I/O: SocketLike protocol
|
|
38
51
|
|
|
39
|
-
The `socketwrapper.SocketLike` protocol,
|
|
40
|
-
is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
52
|
+
The `socketwrapper.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
41
53
|
|
|
42
54
|
```py
|
|
43
55
|
@typing.runtime_checkable
|
|
@@ -52,15 +64,8 @@ class SocketLike(typing.Protocol):
|
|
|
52
64
|
```
|
|
53
65
|
|
|
54
66
|
Special attention to:
|
|
55
|
-
- [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno)
|
|
56
|
-
|
|
57
|
-
- [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout)
|
|
58
|
-
only requires support for `settimeout(.0)` (
|
|
59
|
-
[non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)
|
|
60
|
-
), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError)
|
|
61
|
-
for any other value is fully supported in which case
|
|
62
|
-
[selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector)
|
|
63
|
-
will be used for synchronous operations.
|
|
67
|
+
- [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
|
|
68
|
+
- [settimeout](https://docs.python.org/3.14/library/socket.html#socket.socket.settimeout) must support `settimeout(.0)` ([non-blocking semantics](https://docs.python.org/3.14/library/socket.html#notes-on-socket-timeouts)), raising [ValueError](https://docs.python.org/3.14/library/exceptions.html#ValueError) for any other value will be handled, relying on [selectors.DefaultSelector](https://docs.python.org/3.14/library/selectors.html#selectors.DefaultSelector) for synchronous operations.
|
|
64
69
|
|
|
65
70
|
## Usage
|
|
66
71
|
|
|
@@ -80,11 +85,31 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
|
|
|
80
85
|
else:
|
|
81
86
|
child_writer.send(b'Hello world!')
|
|
82
87
|
```
|
|
83
|
-
|
|
84
88
|
```
|
|
85
89
|
Message b'Hello world!' received
|
|
86
90
|
```
|
|
87
91
|
|
|
92
|
+
### Simple IPC with pipe using msgpack
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import os
|
|
96
|
+
import socketwrapper
|
|
97
|
+
import socketwrapper.framing as framing
|
|
98
|
+
|
|
99
|
+
with socketwrapper.pipe(framing=framing.MsgPack()) as (parent_reader, child_writer):
|
|
100
|
+
child_writer.inheritable = True
|
|
101
|
+
child_pid = os.fork() # replace with your own process fork/spawn logic
|
|
102
|
+
child_writer.inheritable = False # important, prevent socket leaks!
|
|
103
|
+
|
|
104
|
+
if child_pid:
|
|
105
|
+
print(f'Message {parent_reader.recv()!r} received')
|
|
106
|
+
else:
|
|
107
|
+
child_writer.send({'data': b'Hello world!'})
|
|
108
|
+
```
|
|
109
|
+
```
|
|
110
|
+
Message {'data': b'Hello world!'} received
|
|
111
|
+
```
|
|
112
|
+
|
|
88
113
|
### Simple asyncio IPC with pipe
|
|
89
114
|
|
|
90
115
|
```python
|
|
@@ -105,7 +130,6 @@ with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
|
|
|
105
130
|
|
|
106
131
|
asyncio.run(parent(parent_reader) if child_pid else child(child_writer))
|
|
107
132
|
```
|
|
108
|
-
|
|
109
133
|
```
|
|
110
134
|
Message b'Hello world!' received
|
|
111
135
|
```
|
|
@@ -129,7 +153,6 @@ with socketwrapper.socketpair(framing=True) as (parent_duplex, child_duplex):
|
|
|
129
153
|
print(f'Message {child_duplex.recv()!r} received in child')
|
|
130
154
|
child_duplex.send(b'Hello parent!')
|
|
131
155
|
```
|
|
132
|
-
|
|
133
156
|
```
|
|
134
157
|
Message b'Hello child!' received in child
|
|
135
158
|
Message b'Hello parent!' received in parent
|
|
@@ -153,25 +176,46 @@ def child(conn: multiprocessing.connection.Connection) -> None:
|
|
|
153
176
|
|
|
154
177
|
asyncio.run(main())
|
|
155
178
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
179
|
+
if __name__ == '__main__':
|
|
180
|
+
parent_conn, child_conn = multiprocessing.Pipe()
|
|
181
|
+
with parent_conn, child_conn:
|
|
182
|
+
child_process = multiprocessing.Process(target=child, args=(child_conn,))
|
|
183
|
+
child_process.start()
|
|
160
184
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
parent_conn.send_bytes(b'Hello child!')
|
|
186
|
+
print(f'Message {parent_conn.recv_bytes()!r} received in parent')
|
|
187
|
+
child_process.join(1)
|
|
164
188
|
```
|
|
165
|
-
|
|
166
189
|
```
|
|
167
190
|
Message b'Hello child!' received in child
|
|
168
191
|
Message b'Hello parent!' received in parent
|
|
169
192
|
```
|
|
170
193
|
|
|
194
|
+
### Socketwrapper for cross-interpreter communication
|
|
195
|
+
|
|
196
|
+
```py
|
|
197
|
+
import concurrent.futures
|
|
198
|
+
import socketwrapper
|
|
199
|
+
|
|
200
|
+
def child(child_fileno: int) -> None:
|
|
201
|
+
child_writer = socketwrapper.MessageWriter(child_fileno)
|
|
202
|
+
child_writer.send(b'Hello World')
|
|
203
|
+
|
|
204
|
+
if __name__ == '__main__':
|
|
205
|
+
with (socketwrapper.pipe(framing=True) as (parent_reader, child_writer),
|
|
206
|
+
concurrent.futures.InterpreterPoolExecutor() as pool):
|
|
207
|
+
pool.submit(child, child_writer.fileno())
|
|
208
|
+
print(f'Message {parent_reader.recv()!r} received')
|
|
209
|
+
```
|
|
210
|
+
```
|
|
211
|
+
Message b'Hello World' received
|
|
212
|
+
```
|
|
213
|
+
|
|
171
214
|
### Custom socketwrapper framing with progress
|
|
172
215
|
|
|
173
216
|
```py
|
|
174
217
|
import collections.abc
|
|
218
|
+
import io
|
|
175
219
|
import itertools
|
|
176
220
|
import os
|
|
177
221
|
import socketwrapper
|
|
@@ -195,7 +239,7 @@ def progress(arrow: str, size: int, min_chunk: int = 1024) -> collections.abc.Ge
|
|
|
195
239
|
class ProgressFraming(socketwrapper.framing.VarIntBytes):
|
|
196
240
|
|
|
197
241
|
@classmethod
|
|
198
|
-
def frames(cls, buffer:
|
|
242
|
+
def frames(cls, buffer: io.BytesIO) -> collections.abc.Generator[int, None, None]:
|
|
199
243
|
frames = super().frames(buffer)
|
|
200
244
|
yield from itertools.islice(frames, 2)
|
|
201
245
|
yield from progress('>', next(frames))
|
|
@@ -4,8 +4,8 @@ build-backend = 'setuptools.build_meta'
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'socketwrapper'
|
|
7
|
-
description = 'high level socket and pipe
|
|
8
|
-
version = '0.0.
|
|
7
|
+
description = 'high level socket and pipe wrappers'
|
|
8
|
+
version = '0.0.2'
|
|
9
9
|
requires-python = '>=3.12'
|
|
10
10
|
keywords = ['socket', 'pipe', 'ipc', 'asyncio']
|
|
11
11
|
readme = { file = 'README.md', content-type = 'text/markdown' }
|
|
@@ -22,7 +22,7 @@ authors = [
|
|
|
22
22
|
{ name = 'Felipe A Hernandez' },
|
|
23
23
|
{ email = 'ergoithz@gmail.com' },
|
|
24
24
|
]
|
|
25
|
-
dependencies = ['typing-extensions >= 4.
|
|
25
|
+
dependencies = ['typing-extensions >= 4.6 ; python_version < "3.13"']
|
|
26
26
|
|
|
27
27
|
[tool.setuptools.package-data]
|
|
28
28
|
socketwrapper = ["py.typed"]
|
|
@@ -37,8 +37,10 @@ exclude = [
|
|
|
37
37
|
pythonVersion = '3.12'
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
|
+
msgpack = ['msgpack']
|
|
40
41
|
dev = [
|
|
41
42
|
'coverage',
|
|
43
|
+
'msgpack',
|
|
42
44
|
'ruff',
|
|
43
45
|
'wheel',
|
|
44
46
|
'yapf',
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import collections.abc
|
|
5
|
+
import io
|
|
5
6
|
import os
|
|
6
7
|
import selectors
|
|
7
8
|
import socket
|
|
@@ -22,8 +23,8 @@ from . import _utils, framing, protocols
|
|
|
22
23
|
__all__ = (
|
|
23
24
|
# type aliases and protocols
|
|
24
25
|
'MessageFraming',
|
|
25
|
-
'
|
|
26
|
-
'
|
|
26
|
+
'SendPayload', # FIXME(docs): deprecated since 0.0.2
|
|
27
|
+
'SizedBuffer',
|
|
27
28
|
'SocketLike',
|
|
28
29
|
'SocketOrDescriptor',
|
|
29
30
|
|
|
@@ -47,10 +48,11 @@ __all__ = (
|
|
|
47
48
|
DEFAULT_SOCKETPAIR_FAMILY = getattr(socket, 'AF_UNIX', socket.AF_INET)
|
|
48
49
|
DEFAULT_FRAMING = framing.VarIntBytes
|
|
49
50
|
|
|
50
|
-
|
|
51
51
|
SocketLike = protocols.SocketLike
|
|
52
52
|
MessageFraming = protocols.MessageFraming
|
|
53
53
|
SocketOrDescriptor = SocketLike | protocols.HasFileno | int
|
|
54
|
+
SizedBuffer = protocols.SizedBuffer
|
|
55
|
+
|
|
54
56
|
RecvSize = _utils.RecvSize
|
|
55
57
|
SendPayload = _utils.SendPayload
|
|
56
58
|
|
|
@@ -137,7 +139,7 @@ class SocketWrapper(_utils.ClosingContext):
|
|
|
137
139
|
"""Configure socket and consume processor."""
|
|
138
140
|
timeout = _utils.timeout_checker(deadline, 'timeout during transmission') if deadline else None
|
|
139
141
|
wait = self._sock.settimeout if timeout else None
|
|
140
|
-
if self._duplex or _utils.
|
|
142
|
+
if self._duplex or not _utils.safe_timeout(self._sock, timeout() if timeout else None):
|
|
141
143
|
self._sock.settimeout(.0)
|
|
142
144
|
wait = self._selector(write=write)
|
|
143
145
|
|
|
@@ -155,33 +157,23 @@ class SocketWrapper(_utils.ClosingContext):
|
|
|
155
157
|
|
|
156
158
|
async def _consume_async(self, processor: collections.abc.Iterable, *, write: bool = False) -> None:
|
|
157
159
|
"""Configure socket as non-blocking and consume processor."""
|
|
158
|
-
|
|
159
|
-
def notify() -> None:
|
|
160
|
-
"""Pass buffer to target and update future (and clean up) on success or error."""
|
|
161
|
-
try:
|
|
162
|
-
if next(iterator, True):
|
|
163
|
-
future.set_result(None)
|
|
164
|
-
remove_io(self)
|
|
165
|
-
|
|
166
|
-
except asyncio.InvalidStateError:
|
|
167
|
-
remove_io(self)
|
|
168
|
-
|
|
169
|
-
except Exception as e:
|
|
170
|
-
future.set_exception(e)
|
|
171
|
-
remove_io(self)
|
|
172
|
-
|
|
173
160
|
loop = asyncio.get_event_loop()
|
|
174
|
-
future = loop.create_future()
|
|
175
|
-
iterator = iter(processor)
|
|
176
161
|
add_io, remove_io = (
|
|
177
162
|
(loop.add_writer, loop.remove_writer) if write else
|
|
178
163
|
(loop.add_reader, loop.remove_reader)
|
|
179
164
|
)
|
|
180
165
|
|
|
181
166
|
self._sock.settimeout(.0)
|
|
182
|
-
|
|
167
|
+
event = asyncio.Event()
|
|
168
|
+
add_io(self, event.set)
|
|
183
169
|
try:
|
|
184
|
-
|
|
170
|
+
for empty in processor:
|
|
171
|
+
if empty:
|
|
172
|
+
event.clear()
|
|
173
|
+
await event.wait()
|
|
174
|
+
|
|
175
|
+
else:
|
|
176
|
+
await asyncio.sleep(0)
|
|
185
177
|
|
|
186
178
|
finally:
|
|
187
179
|
remove_io(self)
|
|
@@ -197,29 +189,29 @@ class SocketReader(SocketWrapper):
|
|
|
197
189
|
super().__init__(sock)
|
|
198
190
|
self._rlock = _utils.CrossLock()
|
|
199
191
|
|
|
200
|
-
def _recv(self, bufsize: RecvSize, timeout: float | None = None) ->
|
|
192
|
+
def _recv(self, bufsize: RecvSize, timeout: float | None = None) -> io.BytesIO:
|
|
201
193
|
"""Receive data from socket as bytearray."""
|
|
202
194
|
with _utils.lock_timeout(self._rlock, timeout=timeout) as deadline:
|
|
203
|
-
buffer =
|
|
195
|
+
buffer = io.BytesIO()
|
|
204
196
|
reader = _utils.reader(self._sock, buffer, bufsize)
|
|
205
197
|
self._consume(reader, deadline)
|
|
206
198
|
return buffer
|
|
207
199
|
|
|
208
|
-
async def _recv_async(self, bufsize: RecvSize) ->
|
|
200
|
+
async def _recv_async(self, bufsize: RecvSize) -> io.BytesIO:
|
|
209
201
|
"""Receive data from socket (async) as bytearray."""
|
|
210
202
|
async with self._rlock:
|
|
211
|
-
buffer =
|
|
212
|
-
reader = _utils.reader(self._sock, buffer, bufsize)
|
|
203
|
+
buffer = io.BytesIO()
|
|
204
|
+
reader = _utils.reader(self._sock, buffer, bufsize, throttle=True)
|
|
213
205
|
await self._consume_async(reader)
|
|
214
206
|
return buffer
|
|
215
207
|
|
|
216
208
|
def recv(self, bufsize: int, timeout: float | None = None) -> bytes:
|
|
217
209
|
"""Receive data from socket as bytes."""
|
|
218
|
-
return
|
|
210
|
+
return self._recv(bufsize, timeout).getvalue()
|
|
219
211
|
|
|
220
212
|
async def recv_async(self, bufsize: int) -> bytes:
|
|
221
213
|
"""Receive data from socket (async) as bytes."""
|
|
222
|
-
return
|
|
214
|
+
return (await self._recv_async(bufsize)).getvalue()
|
|
223
215
|
|
|
224
216
|
|
|
225
217
|
class SocketWriter(SocketWrapper):
|
|
@@ -232,18 +224,26 @@ class SocketWriter(SocketWrapper):
|
|
|
232
224
|
super().__init__(sock)
|
|
233
225
|
self._wlock = _utils.CrossLock()
|
|
234
226
|
|
|
235
|
-
def
|
|
227
|
+
def _send(self, data: SendPayload, timeout: float | None = None) -> None:
|
|
236
228
|
"""Send data to socket."""
|
|
237
229
|
with _utils.lock_timeout(self._wlock, timeout=timeout) as deadline:
|
|
238
230
|
writer = _utils.writer(self._sock, data)
|
|
239
231
|
self._consume(writer, deadline, write=True)
|
|
240
232
|
|
|
241
|
-
async def
|
|
233
|
+
async def _send_async(self, data: SendPayload) -> None:
|
|
242
234
|
"""Send data to socket (async)."""
|
|
243
235
|
async with self._wlock:
|
|
244
|
-
writer = _utils.writer(self._sock, data)
|
|
236
|
+
writer = _utils.writer(self._sock, data, throttle=True)
|
|
245
237
|
await self._consume_async(writer, write=True)
|
|
246
238
|
|
|
239
|
+
def send(self, data: SizedBuffer, timeout: float | None = None) -> None:
|
|
240
|
+
"""Send data to socket."""
|
|
241
|
+
self._send(data, timeout=timeout)
|
|
242
|
+
|
|
243
|
+
async def send_async(self, data: SizedBuffer) -> None:
|
|
244
|
+
"""Send data to socket (async)."""
|
|
245
|
+
await self._send_async(data)
|
|
246
|
+
|
|
247
247
|
|
|
248
248
|
class SocketDuplex(SocketWriter, SocketReader):
|
|
249
249
|
"""Duplex (both readable and writable) socket wrapper."""
|
|
@@ -311,11 +311,11 @@ class MessageWriter(MessageWrapper[typing.Any, W, KSW], typing_extensions.Generi
|
|
|
311
311
|
|
|
312
312
|
def send(self, data: W, timeout: float | None = None) -> None:
|
|
313
313
|
"""Send data to socket."""
|
|
314
|
-
self._raw.
|
|
314
|
+
self._raw._send(self.framing.dumps(data), timeout=timeout)
|
|
315
315
|
|
|
316
316
|
async def send_async(self, data: W) -> None:
|
|
317
317
|
"""Send data to socket (async)."""
|
|
318
|
-
await self._raw.
|
|
318
|
+
await self._raw._send_async(self.framing.dumps(data))
|
|
319
319
|
|
|
320
320
|
|
|
321
321
|
class MessageDuplex[R, W](MessageWriter[W, SocketDuplex], MessageReader[R, SocketDuplex]):
|