socketwrapper 0.0.2__tar.gz → 0.1.0__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.2/socketwrapper.egg-info → socketwrapper-0.1.0}/PKG-INFO +141 -20
- socketwrapper-0.1.0/README.md +407 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/pyproject.toml +21 -3
- socketwrapper-0.1.0/socketwrapper/__init__.py +696 -0
- socketwrapper-0.1.0/socketwrapper/_io.py +527 -0
- socketwrapper-0.1.0/socketwrapper/_nt.py +473 -0
- socketwrapper-0.1.0/socketwrapper/_utils.py +381 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper/_varint.py +6 -1
- socketwrapper-0.1.0/socketwrapper/framing.py +406 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper/protocols.py +19 -6
- {socketwrapper-0.0.2 → socketwrapper-0.1.0/socketwrapper.egg-info}/PKG-INFO +141 -20
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/SOURCES.txt +2 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/requires.txt +3 -1
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/zip-safe +1 -1
- socketwrapper-0.0.2/README.md +0 -287
- socketwrapper-0.0.2/socketwrapper/__init__.py +0 -476
- socketwrapper-0.0.2/socketwrapper/_utils.py +0 -298
- socketwrapper-0.0.2/socketwrapper/framing.py +0 -255
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/LICENSE +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/setup.cfg +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper/py.typed +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/dependency_links.txt +0 -0
- {socketwrapper-0.0.2 → socketwrapper-0.1.0}/socketwrapper.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: socketwrapper
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: high level socket and pipe wrappers
|
|
5
5
|
Author: Felipe A Hernandez
|
|
6
6
|
Author-email: ergoithz@gmail.com
|
|
@@ -27,11 +27,12 @@ License: MIT License
|
|
|
27
27
|
SOFTWARE.
|
|
28
28
|
|
|
29
29
|
Project-URL: homepage, https://gitlab.com/ergoithz/socketwrapper
|
|
30
|
+
Project-URL: repository, https://gitlab.com/ergoithz/socketwrapper
|
|
30
31
|
Project-URL: issue-tracker, https://gitlab.com/ergoithz/socketwrapper/-/issues
|
|
31
32
|
Project-URL: release-notes, https://gitlab.com/ergoithz/socketwrapper/-/releases
|
|
32
33
|
Project-URL: issue-new, https://gitlab.com/ergoithz/socketwrapper/-/issues/new
|
|
33
34
|
Project-URL: donations, https://ko-fi.com/s26me
|
|
34
|
-
Keywords: socket,pipe,ipc,asyncio
|
|
35
|
+
Keywords: socket,pipe,ipc,asyncio,multiprocessing
|
|
35
36
|
Classifier: Framework :: AsyncIO
|
|
36
37
|
Classifier: Intended Audience :: Developers
|
|
37
38
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -49,30 +50,49 @@ Requires-Dist: coverage; extra == "dev"
|
|
|
49
50
|
Requires-Dist: msgpack; extra == "dev"
|
|
50
51
|
Requires-Dist: ruff; extra == "dev"
|
|
51
52
|
Requires-Dist: wheel; extra == "dev"
|
|
52
|
-
Requires-Dist:
|
|
53
|
+
Requires-Dist: uvloop; sys_platform != "win32" and extra == "dev"
|
|
53
54
|
Dynamic: license-file
|
|
54
55
|
|
|
55
56
|
# socketwrapper
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
High level wrappers for socket and pipe IO, thread-safe, asyncio-native and supporting multiprocessing.
|
|
59
|
+
|
|
60
|
+
Features:
|
|
58
61
|
|
|
59
62
|
- Thread-safe within both threads and asyncio realms, and between them.
|
|
60
63
|
- Managed sync `recv` and `send` operations with timeouts.
|
|
61
64
|
- Native asyncio `recv_async` and `send_async` operations.
|
|
62
|
-
- Pluggable message
|
|
63
|
-
- Pluggable I/O
|
|
65
|
+
- Pluggable message frame and serialization protocols for stream sockets, and serialization for pipes, datagram sockets and Windows message pipes.
|
|
66
|
+
- Pluggable I/O (backed by supported OS descriptors).
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
Built-in message protocols (framing):
|
|
66
69
|
|
|
67
70
|
- `socketwrapper.framing.VarIntBytes`: varint-headered bytes (default with `framing=True`).
|
|
68
|
-
- `socketwrapper.framing.MultiprocessingBytes
|
|
69
|
-
- `socketwrapper.framing.Multiprocessing
|
|
70
|
-
- `socketwrapper.framing.
|
|
71
|
+
- `socketwrapper.framing.MultiprocessingBytes`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
|
|
72
|
+
- `socketwrapper.framing.Multiprocessing`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
|
|
73
|
+
- `socketwrapper.framing.MultiprocessingPipeBytes`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` bytes, or alias of `socketwrapper.framing.MultiprocessingBytes` in other platforms.
|
|
74
|
+
- `socketwrapper.framing.MultiprocessingPipe`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` pickled data, or alias of `socketwrapper.framing.Multiprocessing` in other platforms.
|
|
75
|
+
- `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data.
|
|
76
|
+
|
|
77
|
+
## Limitations on Windows
|
|
78
|
+
|
|
79
|
+
> 🛈 Due platform limitations, `sockerwrapper` has to resort on polling for non-overlapped pipes, and also [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes when not using [asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop). See [Python documentation about asyncio Platform Support](https://docs.python.org/3/library/asyncio-platforms.html#asyncio-platform-support) for details.
|
|
80
|
+
|
|
81
|
+
> ⚠ Due performance considerations, avoid:
|
|
82
|
+
> - Wrapping anonymous pipes (as returned by [os.pipe](https://docs.python.org/3/library/os.html#os.pipe)) whenever possible.
|
|
83
|
+
> - Asynchronous operations on overlapped named pipes when using [asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop).
|
|
84
|
+
|
|
85
|
+
For your convenience, `socketwrapper.pipe` will create overlapped named pipes on Windows.
|
|
86
|
+
|
|
87
|
+
[asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop) (default on Windows) works with both sockets and [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes. Non-overlapped pipes will use polling.
|
|
88
|
+
|
|
89
|
+
[asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop) only natively supports wrapping sockets when manually configured as event loop, all pipes will use polling.
|
|
71
90
|
|
|
72
91
|
## Motivation
|
|
73
92
|
|
|
93
|
+
- I just wanted to send/recv from sockets (and pipes) for IPC without having to dig up 40 years worth of quirks, with and without asyncio.
|
|
74
94
|
- 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.
|
|
95
|
+
- Most implementations got either hardcoded messaging protocols or require a fixed-size header, or just hardcode their own socket initialization like [asyncio.streams](https://docs.python.org/3.14/library/asyncio-stream.html).
|
|
76
96
|
- No implementation was thread-safe between both asyncio and threading realms.
|
|
77
97
|
- No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
|
|
78
98
|
|
|
@@ -89,13 +109,66 @@ uv pip install 'socketwrapper[msgpack]'
|
|
|
89
109
|
|
|
90
110
|
## Changelog
|
|
91
111
|
|
|
92
|
-
###
|
|
112
|
+
### 0.1.0 - 2026.01.22
|
|
113
|
+
|
|
114
|
+
#### Breaking
|
|
115
|
+
|
|
116
|
+
- Type `SendPayload` is no longer exposed.
|
|
117
|
+
- `SizedBuffer` protocol is removed, as it was causing typing annoyances.
|
|
118
|
+
- `SocketLike` protocol now requires `recv_into`, `get_inheritable` and `set_inheritable` implementations.
|
|
119
|
+
|
|
120
|
+
#### Features
|
|
121
|
+
|
|
122
|
+
- Added support for Windows sockets and pipes.
|
|
123
|
+
- Added support for datagram sockets.
|
|
124
|
+
- Added support for blind `recv` operations without explicit size (omitted or `None`), returning the first chunk of data for stream sockets, or a whole datagram for datagram sockets whatever its size.
|
|
125
|
+
- Added `asyncio` event loop feature detection with multiple strategies.
|
|
126
|
+
- Added `StreamEOFError` exception (inheriting and replacing [EOFError](https://docs.python.org/3.14/library/exceptions.html#EOFError)), with a `data` attribute exposing partial result.
|
|
127
|
+
- Added `StreamTimeoutError` exception (inheriting and replacing [TimeoutError](https://docs.python.org/3.14/library/exceptions.html#TimeoutError)), with a `data` attribute exposing partial result.
|
|
128
|
+
- Added `StreamCancelledError`: exception (inheriting and replacing [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError)), with `data` attribute exposing partial result.
|
|
129
|
+
- For **Python 3.12** this is an alias of [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError) due [cpython#113848](https://github.com/python/cpython/issues/113848).
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
#### Changes
|
|
133
|
+
|
|
134
|
+
- For datagram socket-likes (`protocols.ExtendedSocketLike.type` defined as anything other than `socket.SOCK_STREAM`):
|
|
135
|
+
- Protocol `protocols.SocketLike.recv` requires handing new `flags=socket.MSG_PEEK` parameter.
|
|
136
|
+
- Protocol `protocols.SocketLike.recv_into` requires handing both new `bufsize=0` and `flags=socket.MSG_PEEK` parameters.
|
|
137
|
+
- Helper `socketwrapper.pipe`creates overlapped named pipes on **Windows** instead of `os.pipe` anonymous pipes to support asynchronous IO.
|
|
138
|
+
- Improved compatibility for third party asyncio event loops ([uvloop](https://github.com/MagicStack/uvloop) tested).
|
|
139
|
+
- SocketWrappers are now serializable by [multiprocessing](https://docs.python.org/3/library/multiprocessing.html).
|
|
140
|
+
- Optimize `framing.MsgPack` headless stream read operation logic.
|
|
141
|
+
|
|
142
|
+
#### Bugfixes
|
|
143
|
+
|
|
144
|
+
- `SocketWriter.send` and `SocketWriter.send_async` now return its written byte count.
|
|
145
|
+
- Synchronous recv zero timeout `recv(..., timeout=0)` is now handled consistently.
|
|
146
|
+
- `socketwrapper.socketpair` now always returns full duplex socketwrappers.
|
|
147
|
+
|
|
148
|
+
### 0.0.3 - 2025.11.08
|
|
149
|
+
|
|
150
|
+
#### Features
|
|
151
|
+
|
|
152
|
+
- Optional protocol `protocols.ExtendedSocketLike.recv_into`.
|
|
153
|
+
- `recv` operations can now use `sock.recv_into` (if available) to reduce memory allocation overhead.
|
|
154
|
+
|
|
155
|
+
### 0.0.2 - 2025.11.05
|
|
156
|
+
|
|
157
|
+
#### Breaking
|
|
93
158
|
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
|
|
159
|
+
- `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
|
|
160
|
+
|
|
161
|
+
#### Features
|
|
162
|
+
- Optional msgpack support (`msgpack` extra).
|
|
163
|
+
|
|
164
|
+
#### Changes
|
|
165
|
+
|
|
166
|
+
- `SizedBuffer` type is now exposed and deprecate `SendPayload` (removal expected in `0.1.0`).
|
|
167
|
+
- `SocketWriter.send` and `SocketWriter.send_async` parameter `data` type is now `SizedBuffer`.
|
|
168
|
+
|
|
169
|
+
#### Bugfixes
|
|
170
|
+
|
|
171
|
+
- Busy reads no longer blocking asyncio event loop.
|
|
99
172
|
|
|
100
173
|
## Documentation
|
|
101
174
|
|
|
@@ -103,23 +176,28 @@ None other than this README, life's too short and I'm too busy with real life, i
|
|
|
103
176
|
|
|
104
177
|
### Puggable I/O: SocketLike protocol
|
|
105
178
|
|
|
106
|
-
The `
|
|
179
|
+
The `protocols.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
107
180
|
|
|
108
181
|
```py
|
|
109
182
|
@typing.runtime_checkable
|
|
110
183
|
class SocketLike(typing.Protocol):
|
|
111
184
|
"""Protocol for socket-like objects accepted by socketwrapper socket classes."""
|
|
112
185
|
|
|
186
|
+
def close(self) -> None: ...
|
|
113
187
|
def fileno(self) -> int: ...
|
|
188
|
+
def recv(self, bufsize: int, flags: int = 0, /) -> bytes: ...
|
|
189
|
+
def recv_into(self, buffer: collections.abc.Buffer, nbytes: int = 0, flags: int = 0, /) -> int: ...
|
|
114
190
|
def send(self, data: collections.abc.Buffer, /) -> int: ...
|
|
115
|
-
def recv(self, bufsize: int, /) -> bytes: ...
|
|
116
191
|
def settimeout(self, timeout: float | None, /) -> None: ...
|
|
117
|
-
def
|
|
192
|
+
def get_inheritable(self) -> bool: ...
|
|
193
|
+
def set_inheritable(self, inheritable: bool, /) -> None: ...
|
|
194
|
+
|
|
118
195
|
```
|
|
119
196
|
|
|
120
197
|
Special attention to:
|
|
121
198
|
- [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
|
|
122
199
|
- [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.
|
|
200
|
+
- Unless `SocketLike.type` is [socket.SOCK_STREAM](https://docs.python.org/3.14/library/socket.html#socket.SOCK_STREAM) (or missing), [recv](https://docs.python.org/3.14/library/socket.html#socket.socket.recv) and [recv_into](https://docs.python.org/3.14/library/socket.html#socket.socket.recv_into) `flags` must support `socket.MSG_PEEK`.
|
|
123
201
|
|
|
124
202
|
## Usage
|
|
125
203
|
|
|
@@ -339,3 +417,46 @@ Sending 1048576 bytes!
|
|
|
339
417
|
> 100%
|
|
340
418
|
Received 1048576 bytes!
|
|
341
419
|
```
|
|
420
|
+
|
|
421
|
+
#### Connection handling (`socket.accept`)
|
|
422
|
+
|
|
423
|
+
The connection concept is purposely left out of this library (may change in the future), reasoning:
|
|
424
|
+
- Already existing methods for accepting connections: [socket.accept](https://docs.python.org/3/library/socket.html#socket.socket.accept) and [loop.sock_accept](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.loop.sock_accept), both simple and standard.
|
|
425
|
+
- `socketwrapper` can be already used to wrap the connected socket (thread safe and way simpler than [asyncio Streaming Protocols](https://docs.python.org/3.14/library/asyncio-protocol.html#streaming-protocols)).
|
|
426
|
+
- Server sockets aren't usually shared outside the connection loop between sync and async contexts (unnecessary `CrossLock`).
|
|
427
|
+
|
|
428
|
+
So just wrap accepted connection sockets with `socketwrapper.SocketDuplex`.
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
import asyncio
|
|
432
|
+
import socket
|
|
433
|
+
import socketwrapper
|
|
434
|
+
|
|
435
|
+
async def listen(server: socket.socket) -> None:
|
|
436
|
+
loop = asyncio.get_running_loop()
|
|
437
|
+
while True:
|
|
438
|
+
sock, addr = await loop.sock_accept(server)
|
|
439
|
+
with socketwrapper.SocketDuplex(sock) as sock:
|
|
440
|
+
await sock.send_async(b'message')
|
|
441
|
+
|
|
442
|
+
async def main() -> None:
|
|
443
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
444
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
445
|
+
server.bind(('localhost', 8080))
|
|
446
|
+
server.listen(2)
|
|
447
|
+
server.setblocking(False) # asyncio requires a non-blocking server
|
|
448
|
+
listener = asyncio.create_task(listen(server))
|
|
449
|
+
|
|
450
|
+
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
451
|
+
client.connect(('localhost', 8080))
|
|
452
|
+
with socketwrapper.SocketDuplex(client) as client:
|
|
453
|
+
print(await client.recv_async())
|
|
454
|
+
|
|
455
|
+
listener.cancel() # stop connection handler
|
|
456
|
+
await asyncio.gather(listener, return_exceptions=True)
|
|
457
|
+
server.close()
|
|
458
|
+
|
|
459
|
+
if __name__ == '__main__':
|
|
460
|
+
asyncio.run(main())
|
|
461
|
+
|
|
462
|
+
```
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# socketwrapper
|
|
2
|
+
|
|
3
|
+
High level wrappers for socket and pipe IO, thread-safe, asyncio-native and supporting multiprocessing.
|
|
4
|
+
|
|
5
|
+
Features:
|
|
6
|
+
|
|
7
|
+
- Thread-safe within both threads and asyncio realms, and between them.
|
|
8
|
+
- Managed sync `recv` and `send` operations with timeouts.
|
|
9
|
+
- Native asyncio `recv_async` and `send_async` operations.
|
|
10
|
+
- Pluggable message frame and serialization protocols for stream sockets, and serialization for pipes, datagram sockets and Windows message pipes.
|
|
11
|
+
- Pluggable I/O (backed by supported OS descriptors).
|
|
12
|
+
|
|
13
|
+
Built-in message protocols (framing):
|
|
14
|
+
|
|
15
|
+
- `socketwrapper.framing.VarIntBytes`: varint-headered bytes (default with `framing=True`).
|
|
16
|
+
- `socketwrapper.framing.MultiprocessingBytes`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) bytes.
|
|
17
|
+
- `socketwrapper.framing.Multiprocessing`: [multiprocessing.connection.Connection](https://docs.python.org/3.14/library/multiprocessing.html#connection-objects) pickled data.
|
|
18
|
+
- `socketwrapper.framing.MultiprocessingPipeBytes`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` bytes, or alias of `socketwrapper.framing.MultiprocessingBytes` in other platforms.
|
|
19
|
+
- `socketwrapper.framing.MultiprocessingPipe`: framing for undocumented windows-only `multiprocessing.connection.PipeConnection` pickled data, or alias of `socketwrapper.framing.Multiprocessing` in other platforms.
|
|
20
|
+
- `socketwrapper.framing.MsgPack` (if `msgpack` is available): unheadered [msgpack](https://pypi.org/project/msgpack/) data.
|
|
21
|
+
|
|
22
|
+
## Limitations on Windows
|
|
23
|
+
|
|
24
|
+
> 🛈 Due platform limitations, `sockerwrapper` has to resort on polling for non-overlapped pipes, and also [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes when not using [asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop). See [Python documentation about asyncio Platform Support](https://docs.python.org/3/library/asyncio-platforms.html#asyncio-platform-support) for details.
|
|
25
|
+
|
|
26
|
+
> ⚠ Due performance considerations, avoid:
|
|
27
|
+
> - Wrapping anonymous pipes (as returned by [os.pipe](https://docs.python.org/3/library/os.html#os.pipe)) whenever possible.
|
|
28
|
+
> - Asynchronous operations on overlapped named pipes when using [asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop).
|
|
29
|
+
|
|
30
|
+
For your convenience, `socketwrapper.pipe` will create overlapped named pipes on Windows.
|
|
31
|
+
|
|
32
|
+
[asyncio.ProactorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.ProactorEventLoop) (default on Windows) works with both sockets and [overlapped](https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-overlapped-input-and-output) named pipes. Non-overlapped pipes will use polling.
|
|
33
|
+
|
|
34
|
+
[asyncio.SelectorEventLoop](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.SelectorEventLoop) only natively supports wrapping sockets when manually configured as event loop, all pipes will use polling.
|
|
35
|
+
|
|
36
|
+
## Motivation
|
|
37
|
+
|
|
38
|
+
- I just wanted to send/recv from sockets (and pipes) for IPC without having to dig up 40 years worth of quirks, with and without asyncio.
|
|
39
|
+
- 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.
|
|
40
|
+
- Most implementations got either hardcoded messaging protocols or require a fixed-size header, or just hardcode their own socket initialization like [asyncio.streams](https://docs.python.org/3.14/library/asyncio-stream.html).
|
|
41
|
+
- No implementation was thread-safe between both asyncio and threading realms.
|
|
42
|
+
- No implementation directly supported wrapping multiprocessing Connections into an asyncio-native interface.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
uv pip install socketwrapper
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or with optional [msgpack](https://pypi.org/project/msgpack/) support.
|
|
51
|
+
```sh
|
|
52
|
+
uv pip install 'socketwrapper[msgpack]'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Changelog
|
|
56
|
+
|
|
57
|
+
### 0.1.0 - 2026.01.22
|
|
58
|
+
|
|
59
|
+
#### Breaking
|
|
60
|
+
|
|
61
|
+
- Type `SendPayload` is no longer exposed.
|
|
62
|
+
- `SizedBuffer` protocol is removed, as it was causing typing annoyances.
|
|
63
|
+
- `SocketLike` protocol now requires `recv_into`, `get_inheritable` and `set_inheritable` implementations.
|
|
64
|
+
|
|
65
|
+
#### Features
|
|
66
|
+
|
|
67
|
+
- Added support for Windows sockets and pipes.
|
|
68
|
+
- Added support for datagram sockets.
|
|
69
|
+
- Added support for blind `recv` operations without explicit size (omitted or `None`), returning the first chunk of data for stream sockets, or a whole datagram for datagram sockets whatever its size.
|
|
70
|
+
- Added `asyncio` event loop feature detection with multiple strategies.
|
|
71
|
+
- Added `StreamEOFError` exception (inheriting and replacing [EOFError](https://docs.python.org/3.14/library/exceptions.html#EOFError)), with a `data` attribute exposing partial result.
|
|
72
|
+
- Added `StreamTimeoutError` exception (inheriting and replacing [TimeoutError](https://docs.python.org/3.14/library/exceptions.html#TimeoutError)), with a `data` attribute exposing partial result.
|
|
73
|
+
- Added `StreamCancelledError`: exception (inheriting and replacing [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError)), with `data` attribute exposing partial result.
|
|
74
|
+
- For **Python 3.12** this is an alias of [asyncio.CancelledError](https://docs.python.org/3.14/library/asyncio-exceptions.html#asyncio.CancelledError) due [cpython#113848](https://github.com/python/cpython/issues/113848).
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
#### Changes
|
|
78
|
+
|
|
79
|
+
- For datagram socket-likes (`protocols.ExtendedSocketLike.type` defined as anything other than `socket.SOCK_STREAM`):
|
|
80
|
+
- Protocol `protocols.SocketLike.recv` requires handing new `flags=socket.MSG_PEEK` parameter.
|
|
81
|
+
- Protocol `protocols.SocketLike.recv_into` requires handing both new `bufsize=0` and `flags=socket.MSG_PEEK` parameters.
|
|
82
|
+
- Helper `socketwrapper.pipe`creates overlapped named pipes on **Windows** instead of `os.pipe` anonymous pipes to support asynchronous IO.
|
|
83
|
+
- Improved compatibility for third party asyncio event loops ([uvloop](https://github.com/MagicStack/uvloop) tested).
|
|
84
|
+
- SocketWrappers are now serializable by [multiprocessing](https://docs.python.org/3/library/multiprocessing.html).
|
|
85
|
+
- Optimize `framing.MsgPack` headless stream read operation logic.
|
|
86
|
+
|
|
87
|
+
#### Bugfixes
|
|
88
|
+
|
|
89
|
+
- `SocketWriter.send` and `SocketWriter.send_async` now return its written byte count.
|
|
90
|
+
- Synchronous recv zero timeout `recv(..., timeout=0)` is now handled consistently.
|
|
91
|
+
- `socketwrapper.socketpair` now always returns full duplex socketwrappers.
|
|
92
|
+
|
|
93
|
+
### 0.0.3 - 2025.11.08
|
|
94
|
+
|
|
95
|
+
#### Features
|
|
96
|
+
|
|
97
|
+
- Optional protocol `protocols.ExtendedSocketLike.recv_into`.
|
|
98
|
+
- `recv` operations can now use `sock.recv_into` (if available) to reduce memory allocation overhead.
|
|
99
|
+
|
|
100
|
+
### 0.0.2 - 2025.11.05
|
|
101
|
+
|
|
102
|
+
#### Breaking
|
|
103
|
+
|
|
104
|
+
- `MessageFraming.frames` and `MessageFraming.loads` now receive `io.BytesIO` instead of `bytearray`.
|
|
105
|
+
|
|
106
|
+
#### Features
|
|
107
|
+
- Optional msgpack support (`msgpack` extra).
|
|
108
|
+
|
|
109
|
+
#### Changes
|
|
110
|
+
|
|
111
|
+
- `SizedBuffer` type is now exposed and deprecate `SendPayload` (removal expected in `0.1.0`).
|
|
112
|
+
- `SocketWriter.send` and `SocketWriter.send_async` parameter `data` type is now `SizedBuffer`.
|
|
113
|
+
|
|
114
|
+
#### Bugfixes
|
|
115
|
+
|
|
116
|
+
- Busy reads no longer blocking asyncio event loop.
|
|
117
|
+
|
|
118
|
+
## Documentation
|
|
119
|
+
|
|
120
|
+
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/).
|
|
121
|
+
|
|
122
|
+
### Puggable I/O: SocketLike protocol
|
|
123
|
+
|
|
124
|
+
The `protocols.SocketLike` protocol, a small subset the socket interface, is all what's required for any object to be wrap-able by `socketwrapper`.
|
|
125
|
+
|
|
126
|
+
```py
|
|
127
|
+
@typing.runtime_checkable
|
|
128
|
+
class SocketLike(typing.Protocol):
|
|
129
|
+
"""Protocol for socket-like objects accepted by socketwrapper socket classes."""
|
|
130
|
+
|
|
131
|
+
def close(self) -> None: ...
|
|
132
|
+
def fileno(self) -> int: ...
|
|
133
|
+
def recv(self, bufsize: int, flags: int = 0, /) -> bytes: ...
|
|
134
|
+
def recv_into(self, buffer: collections.abc.Buffer, nbytes: int = 0, flags: int = 0, /) -> int: ...
|
|
135
|
+
def send(self, data: collections.abc.Buffer, /) -> int: ...
|
|
136
|
+
def settimeout(self, timeout: float | None, /) -> None: ...
|
|
137
|
+
def get_inheritable(self) -> bool: ...
|
|
138
|
+
def set_inheritable(self, inheritable: bool, /) -> None: ...
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Special attention to:
|
|
143
|
+
- [fileno](https://docs.python.org/3.14/library/socket.html#socket.socket.fileno) has to be a valid OS file descriptor.
|
|
144
|
+
- [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.
|
|
145
|
+
- Unless `SocketLike.type` is [socket.SOCK_STREAM](https://docs.python.org/3.14/library/socket.html#socket.SOCK_STREAM) (or missing), [recv](https://docs.python.org/3.14/library/socket.html#socket.socket.recv) and [recv_into](https://docs.python.org/3.14/library/socket.html#socket.socket.recv_into) `flags` must support `socket.MSG_PEEK`.
|
|
146
|
+
|
|
147
|
+
## Usage
|
|
148
|
+
|
|
149
|
+
### Simple IPC with pipe
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import os
|
|
153
|
+
import socketwrapper
|
|
154
|
+
|
|
155
|
+
with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
|
|
156
|
+
child_writer.inheritable = True
|
|
157
|
+
child_pid = os.fork() # replace with your own process fork/spawn logic
|
|
158
|
+
child_writer.inheritable = False # important, prevent socket leaks!
|
|
159
|
+
|
|
160
|
+
if child_pid:
|
|
161
|
+
print(f'Message {parent_reader.recv()!r} received')
|
|
162
|
+
else:
|
|
163
|
+
child_writer.send(b'Hello world!')
|
|
164
|
+
```
|
|
165
|
+
```
|
|
166
|
+
Message b'Hello world!' received
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Simple IPC with pipe using msgpack
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import os
|
|
173
|
+
import socketwrapper
|
|
174
|
+
import socketwrapper.framing as framing
|
|
175
|
+
|
|
176
|
+
with socketwrapper.pipe(framing=framing.MsgPack()) as (parent_reader, child_writer):
|
|
177
|
+
child_writer.inheritable = True
|
|
178
|
+
child_pid = os.fork() # replace with your own process fork/spawn logic
|
|
179
|
+
child_writer.inheritable = False # important, prevent socket leaks!
|
|
180
|
+
|
|
181
|
+
if child_pid:
|
|
182
|
+
print(f'Message {parent_reader.recv()!r} received')
|
|
183
|
+
else:
|
|
184
|
+
child_writer.send({'data': b'Hello world!'})
|
|
185
|
+
```
|
|
186
|
+
```
|
|
187
|
+
Message {'data': b'Hello world!'} received
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Simple asyncio IPC with pipe
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
import asyncio
|
|
194
|
+
import os
|
|
195
|
+
import socketwrapper
|
|
196
|
+
|
|
197
|
+
async def parent(readable: socketwrapper.MessageReader) -> None:
|
|
198
|
+
print(f'Message {await readable.recv_async()!r} received')
|
|
199
|
+
|
|
200
|
+
async def child(writable: socketwrapper.MessageWriter) -> None:
|
|
201
|
+
await writable.send_async(b'Hello world!')
|
|
202
|
+
|
|
203
|
+
with socketwrapper.pipe(framing=True) as (parent_reader, child_writer):
|
|
204
|
+
child_writer.inheritable = True
|
|
205
|
+
child_pid = os.fork() # replace with your own process fork/spawn logic
|
|
206
|
+
child_writer.inheritable = False # important, prevent socket leaks!
|
|
207
|
+
|
|
208
|
+
asyncio.run(parent(parent_reader) if child_pid else child(child_writer))
|
|
209
|
+
```
|
|
210
|
+
```
|
|
211
|
+
Message b'Hello world!' received
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Simple bidirectional IPC with socketpair
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
import os
|
|
218
|
+
import socketwrapper
|
|
219
|
+
|
|
220
|
+
with socketwrapper.socketpair(framing=True) as (parent_duplex, child_duplex):
|
|
221
|
+
child_duplex.inheritable = True
|
|
222
|
+
child_pid = os.fork() # replace with your own process fork/spawn logic
|
|
223
|
+
child_duplex.inheritable = False # important, prevent socket leaks!
|
|
224
|
+
|
|
225
|
+
if child_pid:
|
|
226
|
+
parent_duplex.send(b'Hello child!')
|
|
227
|
+
print(f'Message {parent_duplex.recv()!r} received in parent')
|
|
228
|
+
|
|
229
|
+
else:
|
|
230
|
+
print(f'Message {child_duplex.recv()!r} received in child')
|
|
231
|
+
child_duplex.send(b'Hello parent!')
|
|
232
|
+
```
|
|
233
|
+
```
|
|
234
|
+
Message b'Hello child!' received in child
|
|
235
|
+
Message b'Hello parent!' received in parent
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Socketwrapper with multiprocessing.Pipe and asyncio
|
|
239
|
+
|
|
240
|
+
```py
|
|
241
|
+
import asyncio
|
|
242
|
+
import multiprocessing
|
|
243
|
+
import multiprocessing.connection
|
|
244
|
+
import socketwrapper
|
|
245
|
+
import socketwrapper.framing
|
|
246
|
+
|
|
247
|
+
def child(conn: multiprocessing.connection.Connection) -> None:
|
|
248
|
+
|
|
249
|
+
async def main() -> None:
|
|
250
|
+
with socketwrapper.MessageDuplex(conn, framing=socketwrapper.framing.MultiprocessingBytes) as child_duplex:
|
|
251
|
+
print(f'Message {await child_duplex.recv_async()!r} received in child')
|
|
252
|
+
await child_duplex.send_async(b'Hello parent!')
|
|
253
|
+
|
|
254
|
+
asyncio.run(main())
|
|
255
|
+
|
|
256
|
+
if __name__ == '__main__':
|
|
257
|
+
parent_conn, child_conn = multiprocessing.Pipe()
|
|
258
|
+
with parent_conn, child_conn:
|
|
259
|
+
child_process = multiprocessing.Process(target=child, args=(child_conn,))
|
|
260
|
+
child_process.start()
|
|
261
|
+
|
|
262
|
+
parent_conn.send_bytes(b'Hello child!')
|
|
263
|
+
print(f'Message {parent_conn.recv_bytes()!r} received in parent')
|
|
264
|
+
child_process.join(1)
|
|
265
|
+
```
|
|
266
|
+
```
|
|
267
|
+
Message b'Hello child!' received in child
|
|
268
|
+
Message b'Hello parent!' received in parent
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Socketwrapper for cross-interpreter communication
|
|
272
|
+
|
|
273
|
+
```py
|
|
274
|
+
import concurrent.futures
|
|
275
|
+
import socketwrapper
|
|
276
|
+
|
|
277
|
+
def child(child_fileno: int) -> None:
|
|
278
|
+
child_writer = socketwrapper.MessageWriter(child_fileno)
|
|
279
|
+
child_writer.send(b'Hello World')
|
|
280
|
+
|
|
281
|
+
if __name__ == '__main__':
|
|
282
|
+
with (socketwrapper.pipe(framing=True) as (parent_reader, child_writer),
|
|
283
|
+
concurrent.futures.InterpreterPoolExecutor() as pool):
|
|
284
|
+
pool.submit(child, child_writer.fileno())
|
|
285
|
+
print(f'Message {parent_reader.recv()!r} received')
|
|
286
|
+
```
|
|
287
|
+
```
|
|
288
|
+
Message b'Hello World' received
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Custom socketwrapper framing with progress
|
|
292
|
+
|
|
293
|
+
```py
|
|
294
|
+
import collections.abc
|
|
295
|
+
import io
|
|
296
|
+
import itertools
|
|
297
|
+
import os
|
|
298
|
+
import socketwrapper
|
|
299
|
+
import socketwrapper.framing
|
|
300
|
+
|
|
301
|
+
def progress(arrow: str, size: int, min_chunk: int = 1024) -> collections.abc.Generator[int, None, None]:
|
|
302
|
+
part_size = max(min_chunk, size // 100)
|
|
303
|
+
full_parts, last_size = divmod(size, part_size)
|
|
304
|
+
percent = 100 / (full_parts + 1 if last_size else full_parts)
|
|
305
|
+
|
|
306
|
+
for i in range(full_parts):
|
|
307
|
+
print(f'{arrow} {i * percent:6.2f}%')
|
|
308
|
+
yield part_size
|
|
309
|
+
|
|
310
|
+
if last_size:
|
|
311
|
+
print(f'{arrow} {full_parts * percent:6.2f}%')
|
|
312
|
+
yield last_size
|
|
313
|
+
|
|
314
|
+
print(f'{arrow} 100%')
|
|
315
|
+
|
|
316
|
+
class ProgressFraming(socketwrapper.framing.VarIntBytes):
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def frames(cls, buffer: io.BytesIO) -> collections.abc.Generator[int, None, None]:
|
|
320
|
+
frames = super().frames(buffer)
|
|
321
|
+
yield from itertools.islice(frames, 2)
|
|
322
|
+
yield from progress('>', next(frames))
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def dumps(cls, data: bytes) -> collections.abc.Generator[memoryview, None, None]:
|
|
326
|
+
buffer = memoryview(b''.join(super().dumps(data)))
|
|
327
|
+
for size in progress('<', len(buffer)):
|
|
328
|
+
chunk, buffer = buffer[:size], buffer[size:]
|
|
329
|
+
yield chunk
|
|
330
|
+
|
|
331
|
+
with socketwrapper.socketpair(framing=ProgressFraming) as (parent_duplex, child_duplex):
|
|
332
|
+
child_duplex.inheritable = True
|
|
333
|
+
child_pid = os.fork() # replace with your own multiprocessing fork logic
|
|
334
|
+
child_duplex.inheritable = False # important, prevent socket leaks!
|
|
335
|
+
|
|
336
|
+
if child_pid:
|
|
337
|
+
payload = os.urandom(1024) * 1024
|
|
338
|
+
print(f'Sending {len(payload)} bytes!')
|
|
339
|
+
parent_duplex.send(payload)
|
|
340
|
+
else:
|
|
341
|
+
print(f'Received {len(child_duplex.recv())} bytes!')
|
|
342
|
+
```
|
|
343
|
+
```
|
|
344
|
+
Sending 1048576 bytes!
|
|
345
|
+
< 0.00%
|
|
346
|
+
< 0.99%
|
|
347
|
+
< 1.98%
|
|
348
|
+
< 2.97%
|
|
349
|
+
...
|
|
350
|
+
> 0.99%
|
|
351
|
+
< 13.86%
|
|
352
|
+
> 1.98%
|
|
353
|
+
< 14.85%
|
|
354
|
+
...
|
|
355
|
+
> 91.09%
|
|
356
|
+
< 99.01%
|
|
357
|
+
> 92.08%
|
|
358
|
+
< 100%
|
|
359
|
+
...
|
|
360
|
+
> 98.02%
|
|
361
|
+
> 99.01%
|
|
362
|
+
> 100%
|
|
363
|
+
Received 1048576 bytes!
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
#### Connection handling (`socket.accept`)
|
|
367
|
+
|
|
368
|
+
The connection concept is purposely left out of this library (may change in the future), reasoning:
|
|
369
|
+
- Already existing methods for accepting connections: [socket.accept](https://docs.python.org/3/library/socket.html#socket.socket.accept) and [loop.sock_accept](https://docs.python.org/3.14/library/asyncio-eventloop.html#asyncio.loop.sock_accept), both simple and standard.
|
|
370
|
+
- `socketwrapper` can be already used to wrap the connected socket (thread safe and way simpler than [asyncio Streaming Protocols](https://docs.python.org/3.14/library/asyncio-protocol.html#streaming-protocols)).
|
|
371
|
+
- Server sockets aren't usually shared outside the connection loop between sync and async contexts (unnecessary `CrossLock`).
|
|
372
|
+
|
|
373
|
+
So just wrap accepted connection sockets with `socketwrapper.SocketDuplex`.
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
import asyncio
|
|
377
|
+
import socket
|
|
378
|
+
import socketwrapper
|
|
379
|
+
|
|
380
|
+
async def listen(server: socket.socket) -> None:
|
|
381
|
+
loop = asyncio.get_running_loop()
|
|
382
|
+
while True:
|
|
383
|
+
sock, addr = await loop.sock_accept(server)
|
|
384
|
+
with socketwrapper.SocketDuplex(sock) as sock:
|
|
385
|
+
await sock.send_async(b'message')
|
|
386
|
+
|
|
387
|
+
async def main() -> None:
|
|
388
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
389
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
390
|
+
server.bind(('localhost', 8080))
|
|
391
|
+
server.listen(2)
|
|
392
|
+
server.setblocking(False) # asyncio requires a non-blocking server
|
|
393
|
+
listener = asyncio.create_task(listen(server))
|
|
394
|
+
|
|
395
|
+
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
396
|
+
client.connect(('localhost', 8080))
|
|
397
|
+
with socketwrapper.SocketDuplex(client) as client:
|
|
398
|
+
print(await client.recv_async())
|
|
399
|
+
|
|
400
|
+
listener.cancel() # stop connection handler
|
|
401
|
+
await asyncio.gather(listener, return_exceptions=True)
|
|
402
|
+
server.close()
|
|
403
|
+
|
|
404
|
+
if __name__ == '__main__':
|
|
405
|
+
asyncio.run(main())
|
|
406
|
+
|
|
407
|
+
```
|