simple-socket-client 1.4__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.
Files changed (23) hide show
  1. simple_socket_client-2.0/CHANGELOG.md +105 -0
  2. {simple-socket-client-1.4 → simple_socket_client-2.0}/LICENSE.md +1 -1
  3. simple_socket_client-2.0/MANIFEST.in +1 -0
  4. simple_socket_client-2.0/PKG-INFO +238 -0
  5. simple_socket_client-2.0/README.md +201 -0
  6. simple_socket_client-2.0/pyproject.toml +61 -0
  7. {simple-socket-client-1.4 → simple_socket_client-2.0}/setup.cfg +4 -4
  8. simple_socket_client-2.0/simple_socket_client/__init__.py +302 -0
  9. simple_socket_client-2.0/simple_socket_client/py.typed +0 -0
  10. simple_socket_client-2.0/simple_socket_client.egg-info/PKG-INFO +238 -0
  11. {simple-socket-client-1.4 → simple_socket_client-2.0}/simple_socket_client.egg-info/SOURCES.txt +5 -4
  12. {simple-socket-client-1.4 → simple_socket_client-2.0}/simple_socket_client.egg-info/requires.txt +3 -0
  13. simple_socket_client-2.0/tests/test_simple_socket_client.py +469 -0
  14. simple-socket-client-1.4/MANIFEST.in +0 -3
  15. simple-socket-client-1.4/PKG-INFO +0 -54
  16. simple-socket-client-1.4/README.md +0 -35
  17. simple-socket-client-1.4/requirements.txt +0 -1
  18. simple-socket-client-1.4/setup.py +0 -26
  19. simple-socket-client-1.4/simple_socket_client/__init__.py +0 -114
  20. simple-socket-client-1.4/simple_socket_client.egg-info/PKG-INFO +0 -54
  21. simple-socket-client-1.4/simple_socket_client.egg-info/not-zip-safe +0 -1
  22. {simple-socket-client-1.4 → simple_socket_client-2.0}/simple_socket_client.egg-info/dependency_links.txt +0 -0
  23. {simple-socket-client-1.4 → 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()`.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2023, Alexey Kuznetsov
1
+ Copyright (c) 2023–2026, webtoucher
2
2
 
3
3
  All rights reserved.
4
4
 
@@ -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
+ ![License](https://img.shields.io/badge/License-BSD%203--Clause-green)
41
+ [![Downloads](https://img.shields.io/pypi/dm/simple-socket-client.svg?color=orange)](https://pypi.python.org/pypi/simple-socket-client)
42
+ [![Latest Version](https://img.shields.io/pypi/v/simple-socket-client.svg)](https://pypi.python.org/pypi/simple-socket-client)
43
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/simple-socket-client.svg)](https://pypi.python.org/pypi/simple-socket-client)
44
+ [![tests](https://github.com/webtoucher/simple-socket-client/actions/workflows/tests.yml/badge.svg)](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
+ ![License](https://img.shields.io/badge/License-BSD%203--Clause-green)
4
+ [![Downloads](https://img.shields.io/pypi/dm/simple-socket-client.svg?color=orange)](https://pypi.python.org/pypi/simple-socket-client)
5
+ [![Latest Version](https://img.shields.io/pypi/v/simple-socket-client.svg)](https://pypi.python.org/pypi/simple-socket-client)
6
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/simple-socket-client.svg)](https://pypi.python.org/pypi/simple-socket-client)
7
+ [![tests](https://github.com/webtoucher/simple-socket-client/actions/workflows/tests.yml/badge.svg)](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
+