simple-socket-client 1.5__tar.gz → 2.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.
- simple_socket_client-2.0/CHANGELOG.md +105 -0
- {simple-socket-client-1.5 → simple_socket_client-2.0}/LICENSE.md +1 -1
- simple_socket_client-2.0/MANIFEST.in +1 -0
- simple_socket_client-2.0/PKG-INFO +238 -0
- simple_socket_client-2.0/README.md +201 -0
- simple_socket_client-2.0/pyproject.toml +61 -0
- {simple-socket-client-1.5 → simple_socket_client-2.0}/setup.cfg +4 -4
- simple_socket_client-2.0/simple_socket_client/__init__.py +302 -0
- simple_socket_client-2.0/simple_socket_client/py.typed +0 -0
- simple_socket_client-2.0/simple_socket_client.egg-info/PKG-INFO +238 -0
- {simple-socket-client-1.5 → simple_socket_client-2.0}/simple_socket_client.egg-info/SOURCES.txt +5 -4
- {simple-socket-client-1.5 → simple_socket_client-2.0}/simple_socket_client.egg-info/requires.txt +3 -0
- simple_socket_client-2.0/tests/test_simple_socket_client.py +469 -0
- simple-socket-client-1.5/MANIFEST.in +0 -3
- simple-socket-client-1.5/PKG-INFO +0 -54
- simple-socket-client-1.5/README.md +0 -35
- simple-socket-client-1.5/requirements.txt +0 -1
- simple-socket-client-1.5/setup.py +0 -26
- simple-socket-client-1.5/simple_socket_client/__init__.py +0 -126
- simple-socket-client-1.5/simple_socket_client.egg-info/PKG-INFO +0 -54
- simple-socket-client-1.5/simple_socket_client.egg-info/not-zip-safe +0 -1
- {simple-socket-client-1.5 → simple_socket_client-2.0}/simple_socket_client.egg-info/dependency_links.txt +0 -0
- {simple-socket-client-1.5 → simple_socket_client-2.0}/simple_socket_client.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [2.0] - 2026-11-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `message(data)` event for data the server sends on its own. The previous
|
|
13
|
+
version read the socket only inside `ask()`, so unsolicited bytes stayed in the
|
|
14
|
+
kernel buffer and eventually stalled the connection.
|
|
15
|
+
- `error(error)` event for a socket error; the connection is closed right after
|
|
16
|
+
and `disconnect` follows.
|
|
17
|
+
- `is_connected`, `host` and `port` properties.
|
|
18
|
+
- `simple_socket_client.__version__`, an explicit `__all__` and type hints on
|
|
19
|
+
every public name, so mypy and pyright see real signatures instead of `Any`.
|
|
20
|
+
The `py.typed` marker (PEP 561) is shipped with the package.
|
|
21
|
+
- `tests/`: pytest suite covering the lifecycle, the message flow through both
|
|
22
|
+
`message` and `ask()`, large payloads, concurrent senders, graceful and abrupt
|
|
23
|
+
disconnects, errors and reconnects.
|
|
24
|
+
- GitHub Actions workflows: tests on Python 3.9-3.15 and 3.13t, plus a tag-driven
|
|
25
|
+
release pipeline publishing to PyPI and creating a GitHub Release.
|
|
26
|
+
- `CHANGELOG.md` and `.gitattributes` with `* text=auto eol=lf`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- The serving thread spun at 100% of a CPU core: the loop polled the send queue
|
|
31
|
+
with `get_nowait()` and `continue`. It waits in `select()` now, and the poll
|
|
32
|
+
interval is the only timeout.
|
|
33
|
+
- `connect()` spun as well and hung forever when the connection failed with
|
|
34
|
+
anything but `SimpleSocketClientException`: the exception was lost in the
|
|
35
|
+
worker thread and the `connected` flag never changed.
|
|
36
|
+
- A connection timeout was not reported on Python 3.9: `socket.timeout` is not
|
|
37
|
+
the builtin `TimeoutError` before 3.10, so the error escaped unwrapped.
|
|
38
|
+
- `disconnect()` only raised a flag: the socket was closed on the next loop
|
|
39
|
+
iteration, the messages already queued by `send()` could be dropped, and
|
|
40
|
+
nothing waited for the close. It stops reading, delivers the queue, closes the
|
|
41
|
+
socket and then emits the event.
|
|
42
|
+
- `ask()` shared one `__keep_answer` flag between all callers, so two overlapping
|
|
43
|
+
calls broke each other; every call waits for its own chunk now.
|
|
44
|
+
- `send()` accepted bytes for a connection that was already gone and put them
|
|
45
|
+
into a queue nobody read.
|
|
46
|
+
- A connection reset was not reported: a `recv()` error ended the loop silently.
|
|
47
|
+
- The serving thread blocked in `recv()`, so a silent server could not be
|
|
48
|
+
disconnected and a forgotten `disconnect()` never let the interpreter exit.
|
|
49
|
+
The socket is non-blocking and polled, and the thread is a daemon.
|
|
50
|
+
- `__disconnected()`, `__incoming_messages` and the two-argument
|
|
51
|
+
`__threading()` helper are gone: the loops they belonged to could not report
|
|
52
|
+
their state, and the first two were never called.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- `send()` is safe to call from another thread: the queue and the poll state are
|
|
57
|
+
guarded by a lock. A message larger than the socket buffer is delivered in
|
|
58
|
+
full, because the unwritten tail is retried on the next poll.
|
|
59
|
+
- `ask()` returns the first chunk of data, raises `TimeoutError` when nothing
|
|
60
|
+
comes within the timeout and `SimpleSocketClientException` when the connection
|
|
61
|
+
is closed while waiting. The same chunk is emitted as a `message` event.
|
|
62
|
+
- A server that closed its end of the connection keeps receiving the messages
|
|
63
|
+
already queued, the way a half-closed connection can still be written to.
|
|
64
|
+
- Packaging moved from `setup.py` to `pyproject.toml` (PEP 621, PEP 639);
|
|
65
|
+
metadata, build configuration and pytest settings live in one file, and
|
|
66
|
+
`requirements.txt` is gone.
|
|
67
|
+
- Documentation: README rewritten with the event table, the API table, and the
|
|
68
|
+
framing, threading and compatibility notes.
|
|
69
|
+
|
|
70
|
+
### Breaking
|
|
71
|
+
|
|
72
|
+
- The `connect` and `disconnect` handlers receive `host` and `port` as two
|
|
73
|
+
arguments instead of one `(host, port)` tuple.
|
|
74
|
+
- Python 3.9 or newer is required.
|
|
75
|
+
- `SimpleSocketClientException` wraps the underlying `OSError` (`raise ... from`).
|
|
76
|
+
- `connect()` raises `RuntimeError` for a live connection instead of starting a
|
|
77
|
+
second one.
|
|
78
|
+
- The serving thread is a daemon: a forgotten `disconnect()` no longer keeps the
|
|
79
|
+
interpreter alive, so call it when the queued messages must be delivered.
|
|
80
|
+
|
|
81
|
+
## [1.5] - 2023-03-03
|
|
82
|
+
|
|
83
|
+
- Connection timeout, and the exception of the worker thread is passed to the
|
|
84
|
+
caller of `connect()`.
|
|
85
|
+
|
|
86
|
+
## [1.4] - 2023-02-06
|
|
87
|
+
|
|
88
|
+
- `EventBus.__init__` is called from the client constructor.
|
|
89
|
+
|
|
90
|
+
## [1.3] - 2023-02-06
|
|
91
|
+
|
|
92
|
+
- Replaced the bundled event emitter with the `event-bus` library.
|
|
93
|
+
|
|
94
|
+
## [1.2] - 2023-02-04
|
|
95
|
+
|
|
96
|
+
- Fixed a typo.
|
|
97
|
+
|
|
98
|
+
## [1.1] - 2023-02-04
|
|
99
|
+
|
|
100
|
+
- Dependencies declared in `setup.py`.
|
|
101
|
+
|
|
102
|
+
## [1.0] - 2023-02-04
|
|
103
|
+
|
|
104
|
+
- First published release: TCP client with `connect`, `send()`, `ask()` and
|
|
105
|
+
`disconnect()`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include CHANGELOG.md
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simple-socket-client
|
|
3
|
+
Version: 2.0
|
|
4
|
+
Summary: Simple TCP socket client with select
|
|
5
|
+
Author-email: Alexey Kuznetsov <mirakuru@webtoucher.ru>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/webtoucher/simple-socket-client
|
|
8
|
+
Project-URL: Repository, https://github.com/webtoucher/simple-socket-client
|
|
9
|
+
Project-URL: Issues, https://github.com/webtoucher/simple-socket-client/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/webtoucher/simple-socket-client/blob/master/CHANGELOG.md
|
|
11
|
+
Keywords: socket,tcp,client,networking,event-driven
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
26
|
+
Classifier: Operating System :: POSIX
|
|
27
|
+
Classifier: Operating System :: Unix
|
|
28
|
+
Classifier: Operating System :: MacOS
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.9
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
License-File: LICENSE.md
|
|
33
|
+
Requires-Dist: event-bus~=1.0.2
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# Simple TCP socket client with select
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
[](https://pypi.python.org/pypi/simple-socket-client)
|
|
42
|
+
[](https://pypi.python.org/pypi/simple-socket-client)
|
|
43
|
+
[](https://pypi.python.org/pypi/simple-socket-client)
|
|
44
|
+
[](https://github.com/webtoucher/simple-socket-client/actions/workflows/tests.yml)
|
|
45
|
+
|
|
46
|
+
A TCP client that keeps one connection in a background thread on a `select()` poll
|
|
47
|
+
with no networking framework underneath: you subscribe handlers to events and the
|
|
48
|
+
client calls them. Type hints are shipped with the package, the only runtime
|
|
49
|
+
dependency is the event bus that dispatches those events.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Install it with pip:
|
|
54
|
+
|
|
55
|
+
```shell
|
|
56
|
+
$ pip install simple-socket-client
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Or you can add it as dependency in requirements.txt file of your python application:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
simple-socket-client~=2.0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from simple_socket_client import SimpleSocketClient
|
|
69
|
+
|
|
70
|
+
client = SimpleSocketClient('192.168.0.2', 6666)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@client.on('message')
|
|
74
|
+
def on_message(data: bytes):
|
|
75
|
+
print('Incoming data:', data)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@client.on('disconnect')
|
|
79
|
+
def on_disconnect(host, port):
|
|
80
|
+
print('Connection to %s:%s is closed' % (host, port))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
client.connect(timeout=10)
|
|
84
|
+
|
|
85
|
+
client.send(b'Test') # if you don't need an answer
|
|
86
|
+
answer = client.ask(b'Hi!')
|
|
87
|
+
print(answer.decode())
|
|
88
|
+
|
|
89
|
+
client.disconnect()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`connect()` blocks the calling thread until the connection is established and
|
|
93
|
+
raises `SimpleSocketClientException` when it fails, so there is no need to wait
|
|
94
|
+
for the `connect` event to start using the client.
|
|
95
|
+
|
|
96
|
+
`ask()` is a shorthand for "send and wait for the first chunk of the answer":
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
answer = client.ask(b'PING\r\n', timeout=5)
|
|
100
|
+
if answer == b'PONG\r\n':
|
|
101
|
+
print('the server is alive')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The same client works against [`simple-socket-server`](https://pypi.org/project/simple-socket-server/)
|
|
105
|
+
and any other TCP server:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from simple_socket_server import SimpleSocketServer
|
|
109
|
+
|
|
110
|
+
server = SimpleSocketServer()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@server.on('message')
|
|
114
|
+
def on_message(sock, peer, data):
|
|
115
|
+
server.send(sock, b'Hi, ' + data)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
server.run(host='0.0.0.0', port=6666)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Events
|
|
122
|
+
|
|
123
|
+
Handlers are called one at a time from the thread that serves the connection;
|
|
124
|
+
the `connect` handler is the exception, it is called by `connect()` itself.
|
|
125
|
+
|
|
126
|
+
| Event | Arguments | When it fires |
|
|
127
|
+
|--------------|-----------------|-------------------------------------------------------------------------------|
|
|
128
|
+
| `connect` | `host, port` | the connection is open |
|
|
129
|
+
| `message` | `data` | a chunk of bytes has been received |
|
|
130
|
+
| `disconnect` | `host, port` | the connection is closed; the messages queued by `send()` were delivered first |
|
|
131
|
+
| `error` | `error` | a socket error occurred; the connection is closed and `disconnect` follows |
|
|
132
|
+
|
|
133
|
+
`data` is `bytes`, `error` is an `OSError`.
|
|
134
|
+
|
|
135
|
+
## API
|
|
136
|
+
|
|
137
|
+
| Member | Description |
|
|
138
|
+
|------------------------------------------|-----------------------------------------------------------------------|
|
|
139
|
+
| `SimpleSocketClient(host, port)` | a client for one address; nothing is opened until `connect()` |
|
|
140
|
+
| `connect(timeout=None)` | open the connection; blocks, `SimpleSocketClientException` on failure |
|
|
141
|
+
| `disconnect()` | stop reading, deliver the queue, then close the connection |
|
|
142
|
+
| `send(message)` | queue `bytes` for the server |
|
|
143
|
+
| `ask(message, timeout=1.)` | send `bytes` and return the first chunk of the answer |
|
|
144
|
+
| `is_connected` | `True` while the connection is open and serving messages |
|
|
145
|
+
| `host`, `port` | the address the client connects to |
|
|
146
|
+
| `on(event)`, `emit(...)` | inherited from `event_bus.EventBus` |
|
|
147
|
+
|
|
148
|
+
Notes:
|
|
149
|
+
|
|
150
|
+
- `connect()` raises `RuntimeError` for a live connection: one client object
|
|
151
|
+
serves one connection at a time. After `disconnect()` it can be called again,
|
|
152
|
+
and the queue of the previous connection is delivered first.
|
|
153
|
+
- `send()` never blocks and never truncates: the socket is non-blocking, so the
|
|
154
|
+
bytes are buffered and the unwritten tail is retried on the next poll, so a
|
|
155
|
+
message larger than the socket buffer arrives in full.
|
|
156
|
+
- `send()` and `disconnect()` are safe to call from another thread. Delivery
|
|
157
|
+
happens during the next poll, so it can take up to the poll interval (0.1 s).
|
|
158
|
+
- `disconnect()` delivers the messages already queued, so a `send()` followed by
|
|
159
|
+
a `disconnect()` still reaches the server. From another thread the call waits
|
|
160
|
+
for that to finish, for up to 5 seconds: a server that stopped reading makes the
|
|
161
|
+
rest of the queue be dropped. From an event handler it only raises the flag and
|
|
162
|
+
the serving thread closes the socket itself.
|
|
163
|
+
- **A handler must not raise.** The exception breaks the serving loop, the
|
|
164
|
+
connection is closed and `message` never arrives again; the exception itself is
|
|
165
|
+
printed by the `threading` module. Do the long work in your own thread and
|
|
166
|
+
handle per-message errors inside the handler.
|
|
167
|
+
- `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
|
|
168
|
+
framing: the protocol you put on top has to delimit its own messages, and
|
|
169
|
+
`ask()` returns one chunk, which may hold more than the answer or less than a
|
|
170
|
+
full line.
|
|
171
|
+
- `ask()` and a `message` handler see the same chunk. `ask()` raises
|
|
172
|
+
`TimeoutError` when nothing comes within the timeout and
|
|
173
|
+
`SimpleSocketClientException` when the connection is closed while waiting.
|
|
174
|
+
- The serving thread is a daemon, so a forgotten `disconnect()` does not keep the
|
|
175
|
+
interpreter alive; the messages that were not written yet are lost then.
|
|
176
|
+
- Do not call `sock.close()` yourself: the client owns the socket and drops it
|
|
177
|
+
with an `error` event of `select()` if it goes away behind its back.
|
|
178
|
+
|
|
179
|
+
## Compatibility
|
|
180
|
+
|
|
181
|
+
- Python **3.9 and newer**, including free-threaded builds: CI runs every version
|
|
182
|
+
from 3.9 to 3.15 plus the free-threaded `3.13t`.
|
|
183
|
+
- One runtime dependency: [`event-bus`](https://pypi.org/project/event-bus/).
|
|
184
|
+
- Type hints are shipped with the package (`py.typed`, PEP 561), so mypy and
|
|
185
|
+
pyright see real signatures instead of `Any`.
|
|
186
|
+
- Any client instance is independent: several clients may serve several
|
|
187
|
+
connections in one process.
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```shell
|
|
192
|
+
$ python -m venv .venv
|
|
193
|
+
$ .venv\Scripts\activate # Windows; on POSIX: source .venv/bin/activate
|
|
194
|
+
$ pip install -e ".[dev]" # installs the package and pytest
|
|
195
|
+
$ python -m pytest
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The test suite covers:
|
|
199
|
+
|
|
200
|
+
- the lifecycle: `connect`, `is_connected`, `host`, `port`, a rejected
|
|
201
|
+
connection, a second `connect()` on a live connection and a reconnect after a
|
|
202
|
+
graceful close,
|
|
203
|
+
- the message flow: the `message` event for data the server sends on its own,
|
|
204
|
+
`ask()` round trips, `send()` ordering and a 256 KiB payload in both directions,
|
|
205
|
+
- concurrency: several threads calling `send()` at once,
|
|
206
|
+
- disconnects: the queue delivered before `disconnect()` closes the connection, an
|
|
207
|
+
idempotent `disconnect()`, a close and a reset by the server, and `ask()` that
|
|
208
|
+
waits when the connection goes away,
|
|
209
|
+
- error handling: `send()` and `ask()` on a closed client, an empty message, an
|
|
210
|
+
`ask()` timeout, and a handler that raises,
|
|
211
|
+
- the public API: `__all__`, the version and the package metadata.
|
|
212
|
+
|
|
213
|
+
Project layout:
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
simple_socket_client/__init__.py the whole library
|
|
217
|
+
tests/ test suite
|
|
218
|
+
pyproject.toml metadata, build config and pytest settings
|
|
219
|
+
.github/workflows/ CI: tests on Python 3.9-3.15 and 3.13t, release pipeline
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
To build and verify a release:
|
|
223
|
+
|
|
224
|
+
```shell
|
|
225
|
+
$ pip install build twine
|
|
226
|
+
$ python -m build
|
|
227
|
+
$ python -m twine check dist/*
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Changelog
|
|
231
|
+
|
|
232
|
+
See [CHANGELOG.md](CHANGELOG.md). GitHub Releases are generated from the matching
|
|
233
|
+
entry of that file, so the history is available both in git and on the releases
|
|
234
|
+
page.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
Distributed under the BSD 3-Clause license. See [LICENSE.md](LICENSE.md) for details.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Simple TCP socket client with select
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
[](https://pypi.python.org/pypi/simple-socket-client)
|
|
5
|
+
[](https://pypi.python.org/pypi/simple-socket-client)
|
|
6
|
+
[](https://pypi.python.org/pypi/simple-socket-client)
|
|
7
|
+
[](https://github.com/webtoucher/simple-socket-client/actions/workflows/tests.yml)
|
|
8
|
+
|
|
9
|
+
A TCP client that keeps one connection in a background thread on a `select()` poll
|
|
10
|
+
with no networking framework underneath: you subscribe handlers to events and the
|
|
11
|
+
client calls them. Type hints are shipped with the package, the only runtime
|
|
12
|
+
dependency is the event bus that dispatches those events.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install it with pip:
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
$ pip install simple-socket-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or you can add it as dependency in requirements.txt file of your python application:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
simple-socket-client~=2.0
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from simple_socket_client import SimpleSocketClient
|
|
32
|
+
|
|
33
|
+
client = SimpleSocketClient('192.168.0.2', 6666)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@client.on('message')
|
|
37
|
+
def on_message(data: bytes):
|
|
38
|
+
print('Incoming data:', data)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@client.on('disconnect')
|
|
42
|
+
def on_disconnect(host, port):
|
|
43
|
+
print('Connection to %s:%s is closed' % (host, port))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
client.connect(timeout=10)
|
|
47
|
+
|
|
48
|
+
client.send(b'Test') # if you don't need an answer
|
|
49
|
+
answer = client.ask(b'Hi!')
|
|
50
|
+
print(answer.decode())
|
|
51
|
+
|
|
52
|
+
client.disconnect()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`connect()` blocks the calling thread until the connection is established and
|
|
56
|
+
raises `SimpleSocketClientException` when it fails, so there is no need to wait
|
|
57
|
+
for the `connect` event to start using the client.
|
|
58
|
+
|
|
59
|
+
`ask()` is a shorthand for "send and wait for the first chunk of the answer":
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
answer = client.ask(b'PING\r\n', timeout=5)
|
|
63
|
+
if answer == b'PONG\r\n':
|
|
64
|
+
print('the server is alive')
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The same client works against [`simple-socket-server`](https://pypi.org/project/simple-socket-server/)
|
|
68
|
+
and any other TCP server:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from simple_socket_server import SimpleSocketServer
|
|
72
|
+
|
|
73
|
+
server = SimpleSocketServer()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@server.on('message')
|
|
77
|
+
def on_message(sock, peer, data):
|
|
78
|
+
server.send(sock, b'Hi, ' + data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
server.run(host='0.0.0.0', port=6666)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Events
|
|
85
|
+
|
|
86
|
+
Handlers are called one at a time from the thread that serves the connection;
|
|
87
|
+
the `connect` handler is the exception, it is called by `connect()` itself.
|
|
88
|
+
|
|
89
|
+
| Event | Arguments | When it fires |
|
|
90
|
+
|--------------|-----------------|-------------------------------------------------------------------------------|
|
|
91
|
+
| `connect` | `host, port` | the connection is open |
|
|
92
|
+
| `message` | `data` | a chunk of bytes has been received |
|
|
93
|
+
| `disconnect` | `host, port` | the connection is closed; the messages queued by `send()` were delivered first |
|
|
94
|
+
| `error` | `error` | a socket error occurred; the connection is closed and `disconnect` follows |
|
|
95
|
+
|
|
96
|
+
`data` is `bytes`, `error` is an `OSError`.
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
| Member | Description |
|
|
101
|
+
|------------------------------------------|-----------------------------------------------------------------------|
|
|
102
|
+
| `SimpleSocketClient(host, port)` | a client for one address; nothing is opened until `connect()` |
|
|
103
|
+
| `connect(timeout=None)` | open the connection; blocks, `SimpleSocketClientException` on failure |
|
|
104
|
+
| `disconnect()` | stop reading, deliver the queue, then close the connection |
|
|
105
|
+
| `send(message)` | queue `bytes` for the server |
|
|
106
|
+
| `ask(message, timeout=1.)` | send `bytes` and return the first chunk of the answer |
|
|
107
|
+
| `is_connected` | `True` while the connection is open and serving messages |
|
|
108
|
+
| `host`, `port` | the address the client connects to |
|
|
109
|
+
| `on(event)`, `emit(...)` | inherited from `event_bus.EventBus` |
|
|
110
|
+
|
|
111
|
+
Notes:
|
|
112
|
+
|
|
113
|
+
- `connect()` raises `RuntimeError` for a live connection: one client object
|
|
114
|
+
serves one connection at a time. After `disconnect()` it can be called again,
|
|
115
|
+
and the queue of the previous connection is delivered first.
|
|
116
|
+
- `send()` never blocks and never truncates: the socket is non-blocking, so the
|
|
117
|
+
bytes are buffered and the unwritten tail is retried on the next poll, so a
|
|
118
|
+
message larger than the socket buffer arrives in full.
|
|
119
|
+
- `send()` and `disconnect()` are safe to call from another thread. Delivery
|
|
120
|
+
happens during the next poll, so it can take up to the poll interval (0.1 s).
|
|
121
|
+
- `disconnect()` delivers the messages already queued, so a `send()` followed by
|
|
122
|
+
a `disconnect()` still reaches the server. From another thread the call waits
|
|
123
|
+
for that to finish, for up to 5 seconds: a server that stopped reading makes the
|
|
124
|
+
rest of the queue be dropped. From an event handler it only raises the flag and
|
|
125
|
+
the serving thread closes the socket itself.
|
|
126
|
+
- **A handler must not raise.** The exception breaks the serving loop, the
|
|
127
|
+
connection is closed and `message` never arrives again; the exception itself is
|
|
128
|
+
printed by the `threading` module. Do the long work in your own thread and
|
|
129
|
+
handle per-message errors inside the handler.
|
|
130
|
+
- `message` carries one raw `recv()` chunk of up to 1024 bytes. There is no
|
|
131
|
+
framing: the protocol you put on top has to delimit its own messages, and
|
|
132
|
+
`ask()` returns one chunk, which may hold more than the answer or less than a
|
|
133
|
+
full line.
|
|
134
|
+
- `ask()` and a `message` handler see the same chunk. `ask()` raises
|
|
135
|
+
`TimeoutError` when nothing comes within the timeout and
|
|
136
|
+
`SimpleSocketClientException` when the connection is closed while waiting.
|
|
137
|
+
- The serving thread is a daemon, so a forgotten `disconnect()` does not keep the
|
|
138
|
+
interpreter alive; the messages that were not written yet are lost then.
|
|
139
|
+
- Do not call `sock.close()` yourself: the client owns the socket and drops it
|
|
140
|
+
with an `error` event of `select()` if it goes away behind its back.
|
|
141
|
+
|
|
142
|
+
## Compatibility
|
|
143
|
+
|
|
144
|
+
- Python **3.9 and newer**, including free-threaded builds: CI runs every version
|
|
145
|
+
from 3.9 to 3.15 plus the free-threaded `3.13t`.
|
|
146
|
+
- One runtime dependency: [`event-bus`](https://pypi.org/project/event-bus/).
|
|
147
|
+
- Type hints are shipped with the package (`py.typed`, PEP 561), so mypy and
|
|
148
|
+
pyright see real signatures instead of `Any`.
|
|
149
|
+
- Any client instance is independent: several clients may serve several
|
|
150
|
+
connections in one process.
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```shell
|
|
155
|
+
$ python -m venv .venv
|
|
156
|
+
$ .venv\Scripts\activate # Windows; on POSIX: source .venv/bin/activate
|
|
157
|
+
$ pip install -e ".[dev]" # installs the package and pytest
|
|
158
|
+
$ python -m pytest
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The test suite covers:
|
|
162
|
+
|
|
163
|
+
- the lifecycle: `connect`, `is_connected`, `host`, `port`, a rejected
|
|
164
|
+
connection, a second `connect()` on a live connection and a reconnect after a
|
|
165
|
+
graceful close,
|
|
166
|
+
- the message flow: the `message` event for data the server sends on its own,
|
|
167
|
+
`ask()` round trips, `send()` ordering and a 256 KiB payload in both directions,
|
|
168
|
+
- concurrency: several threads calling `send()` at once,
|
|
169
|
+
- disconnects: the queue delivered before `disconnect()` closes the connection, an
|
|
170
|
+
idempotent `disconnect()`, a close and a reset by the server, and `ask()` that
|
|
171
|
+
waits when the connection goes away,
|
|
172
|
+
- error handling: `send()` and `ask()` on a closed client, an empty message, an
|
|
173
|
+
`ask()` timeout, and a handler that raises,
|
|
174
|
+
- the public API: `__all__`, the version and the package metadata.
|
|
175
|
+
|
|
176
|
+
Project layout:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
simple_socket_client/__init__.py the whole library
|
|
180
|
+
tests/ test suite
|
|
181
|
+
pyproject.toml metadata, build config and pytest settings
|
|
182
|
+
.github/workflows/ CI: tests on Python 3.9-3.15 and 3.13t, release pipeline
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
To build and verify a release:
|
|
186
|
+
|
|
187
|
+
```shell
|
|
188
|
+
$ pip install build twine
|
|
189
|
+
$ python -m build
|
|
190
|
+
$ python -m twine check dist/*
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Changelog
|
|
194
|
+
|
|
195
|
+
See [CHANGELOG.md](CHANGELOG.md). GitHub Releases are generated from the matching
|
|
196
|
+
entry of that file, so the history is available both in git and on the releases
|
|
197
|
+
page.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
Distributed under the BSD 3-Clause license. See [LICENSE.md](LICENSE.md) for details.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simple-socket-client"
|
|
7
|
+
description = "Simple TCP socket client with select"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
license-files = ["LICENSE.md"]
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Alexey Kuznetsov", email = "mirakuru@webtoucher.ru" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["socket", "tcp", "client", "networking", "event-driven"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
"Programming Language :: Python :: 3.15",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Topic :: System :: Networking",
|
|
30
|
+
"Operating System :: Microsoft :: Windows",
|
|
31
|
+
"Operating System :: POSIX",
|
|
32
|
+
"Operating System :: Unix",
|
|
33
|
+
"Operating System :: MacOS",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
dynamic = ["version"]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"event-bus~=1.0.2",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = ["pytest>=7"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/webtoucher/simple-socket-client"
|
|
46
|
+
Repository = "https://github.com/webtoucher/simple-socket-client"
|
|
47
|
+
Issues = "https://github.com/webtoucher/simple-socket-client/issues"
|
|
48
|
+
Changelog = "https://github.com/webtoucher/simple-socket-client/blob/master/CHANGELOG.md"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
packages = ["simple_socket_client"]
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.dynamic]
|
|
54
|
+
version = { attr = "simple_socket_client.__version__" }
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
simple_socket_client = ["py.typed"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = "-ra"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|