simple-socket-client 2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ """
2
+ Simple TCP socket client built on select().
3
+
4
+ The client keeps its connection in a background thread: send() queues the bytes
5
+ for that thread, which polls the socket and calls the handlers registered for its
6
+ events. No third-party networking code is involved, the only runtime dependency
7
+ is the event bus used to dispatch those events.
8
+
9
+ Copyright (c) 2023–2026 webtoucher
10
+ Distributed under the BSD 3-Clause license. See LICENSE.md for more info.
11
+ """
12
+
13
+ import queue
14
+ import select
15
+ import socket
16
+ import threading
17
+
18
+ from collections import deque
19
+ from typing import Deque, Optional
20
+
21
+ from event_bus import EventBus
22
+
23
+ __all__ = ["SimpleSocketClient", "SimpleSocketClientException"]
24
+
25
+ __version__ = "2.0"
26
+
27
+ #: Size of a single recv() chunk; a message event carries one such chunk.
28
+ _RECV_SIZE = 1024
29
+
30
+ #: How long select() waits for socket activity, in seconds.
31
+ _POLL_INTERVAL = 0.1
32
+
33
+ #: How long disconnect() waits for the queued messages before it lets them go.
34
+ _DRAIN_TIMEOUT = 5.0
35
+
36
+
37
+ class SimpleSocketClientException(socket.error):
38
+ """Raised when the connection cannot be opened or is not available."""
39
+
40
+
41
+ class SimpleSocketClient(EventBus):
42
+ """TCP client dispatching events through an event bus.
43
+
44
+ Events, in the order they may arrive:
45
+
46
+ ``connect(host, port)``
47
+ The connection is open. Emitted by connect(), so it is called from the
48
+ thread that called connect().
49
+ ``message(data)``
50
+ A chunk of bytes has been received.
51
+ ``disconnect(host, port)``
52
+ The connection is closed: the server closed it, reset it, a send failed
53
+ or disconnect() was called. Queued messages are delivered before it.
54
+ ``error(error)``
55
+ A socket error occurred; the connection is closed right after and
56
+ ``disconnect`` follows.
57
+
58
+ Every handler but the ``connect`` one is called from the thread that serves
59
+ the connection, one at a time, so a slow handler delays reading and writing.
60
+ **Handlers must not raise**: the exception breaks the serving loop, the
61
+ connection is closed and the exception is printed by the threading module.
62
+ """
63
+
64
+ def __init__(self, host: str, port: int) -> None:
65
+ super().__init__()
66
+ self.__host = host
67
+ self.__port = port
68
+ self.__socket: Optional[socket.socket] = None
69
+ self.__thread: Optional[threading.Thread] = None
70
+ self.__pending = bytearray()
71
+ self.__answers: Deque[queue.Queue] = deque()
72
+ self.__closing = False
73
+ self.__lock = threading.Lock()
74
+
75
+ @property
76
+ def is_connected(self) -> bool:
77
+ """True while the connection is open and serving messages."""
78
+ return self.__socket is not None and not self.__closing
79
+
80
+ @property
81
+ def host(self) -> str:
82
+ """Host the client connects to."""
83
+ return self.__host
84
+
85
+ @property
86
+ def port(self) -> int:
87
+ """Port the client connects to."""
88
+ return self.__port
89
+
90
+ def connect(self, timeout: Optional[float] = None) -> None:
91
+ """Open the connection to host:port.
92
+
93
+ The call blocks until the connection is established or fails. timeout is
94
+ passed to the socket, so None waits as long as the system allows.
95
+ A rejected, unreachable or timed out connection raises
96
+ SimpleSocketClientException.
97
+
98
+ A second connect() on a live connection raises RuntimeError; after
99
+ disconnect() the previous queue is delivered first.
100
+ """
101
+ if self.is_connected:
102
+ raise RuntimeError("the client is already connected")
103
+ thread = self.__thread
104
+ if thread is not None and thread.is_alive():
105
+ # The previous connection is still draining its queue.
106
+ self.__wait_closed(thread)
107
+ sock = socket.socket(
108
+ socket.AF_INET,
109
+ socket.SOCK_STREAM,
110
+ socket.IPPROTO_TCP,
111
+ )
112
+ try:
113
+ sock.settimeout(timeout)
114
+ sock.connect((self.__host, self.__port))
115
+ sock.setblocking(False)
116
+ except socket.timeout as error:
117
+ sock.close()
118
+ raise SimpleSocketClientException(
119
+ "connection to %s:%s timed out" % (self.__host, self.__port)
120
+ ) from error
121
+ except OSError as error:
122
+ sock.close()
123
+ raise SimpleSocketClientException(
124
+ "cannot connect to %s:%s" % (self.__host, self.__port)
125
+ ) from error
126
+ with self.__lock:
127
+ self.__socket = sock
128
+ self.__pending.clear()
129
+ self.__closing = False
130
+ self.__thread = threading.Thread(target=self.__serve, args=(sock,), daemon=True)
131
+ self.__thread.start()
132
+ self.emit("connect", self.__host, self.__port)
133
+
134
+ def disconnect(self) -> None:
135
+ """Close the connection gracefully.
136
+
137
+ The socket stops being read, the messages already queued by send() are
138
+ delivered, and only then the socket is closed and the ``disconnect``
139
+ event is emitted. A call from another thread waits for that to finish, for
140
+ up to 5 seconds (_DRAIN_TIMEOUT): a server that stopped reading makes the
141
+ rest of the queue be dropped. A call from an event handler only raises
142
+ the flag, the serving thread closes the socket itself.
143
+
144
+ Calling it on a closed client does nothing.
145
+ """
146
+ with self.__lock:
147
+ if self.__socket is None:
148
+ return
149
+ self.__closing = True
150
+ thread = self.__thread
151
+ if thread is None or thread is threading.current_thread():
152
+ return
153
+ self.__wait_closed(thread)
154
+
155
+ def send(self, message: bytes) -> None:
156
+ """Queue message for delivery to the server.
157
+
158
+ The socket is non-blocking, so the bytes are buffered and the unwritten
159
+ tail is retried on the next poll; a message larger than the socket buffer
160
+ is delivered in full. Empty messages are ignored.
161
+
162
+ Safe to call from another thread: delivery happens during the next poll,
163
+ so it may take up to the poll interval. Raises
164
+ SimpleSocketClientException when the client is not connected.
165
+ """
166
+ if not message:
167
+ return
168
+ with self.__lock:
169
+ if self.__socket is None or self.__closing:
170
+ raise SimpleSocketClientException("the client is not connected")
171
+ self.__pending.extend(message)
172
+
173
+ def ask(self, message: bytes, timeout: Optional[float] = 1.) -> bytes:
174
+ """Send message and return the first chunk of the answer.
175
+
176
+ The call blocks until data arrives: TimeoutError when nothing comes
177
+ within timeout, SimpleSocketClientException when the client is not
178
+ connected or the connection is closed while waiting. The chunk is
179
+ emitted through the ``message`` event as well, so the two ways of
180
+ receiving it see the same bytes. When several threads wait at once, each
181
+ chunk goes to the oldest of them.
182
+ """
183
+ waiter: queue.Queue = queue.Queue()
184
+ with self.__lock:
185
+ self.__answers.append(waiter)
186
+ try:
187
+ self.send(message)
188
+ try:
189
+ answer = waiter.get(timeout=timeout)
190
+ except queue.Empty:
191
+ raise TimeoutError(
192
+ "no answer from %s:%s in %s seconds"
193
+ % (self.__host, self.__port, timeout)
194
+ ) from None
195
+ finally:
196
+ with self.__lock:
197
+ if waiter in self.__answers:
198
+ self.__answers.remove(waiter)
199
+ if answer is None:
200
+ raise SimpleSocketClientException("the connection is closed")
201
+ return answer
202
+
203
+ def __serve(self, sock: socket.socket) -> None:
204
+ """Poll the socket, write the queue and hand the answers over."""
205
+ try:
206
+ while True:
207
+ with self.__lock:
208
+ pending = bool(self.__pending)
209
+ if self.__closing and not pending:
210
+ break
211
+ try:
212
+ readable, writable, _ = select.select(
213
+ [] if self.__closing else [sock],
214
+ [sock] if pending else [],
215
+ [],
216
+ _POLL_INTERVAL,
217
+ )
218
+ except (OSError, ValueError) as error:
219
+ # select() broke: the socket was closed behind our back or
220
+ # is unusable, so the connection cannot be served any more.
221
+ self.emit("error", error)
222
+ break
223
+ if writable and not self.__flush(sock):
224
+ break
225
+ if readable and not self.__receive(sock):
226
+ break
227
+ finally:
228
+ self.__close(sock)
229
+
230
+ def __flush(self, sock: socket.socket) -> bool:
231
+ """Write as much of the queue as the socket accepts. False on failure."""
232
+ failure: Optional[OSError] = None
233
+ with self.__lock:
234
+ if not self.__pending:
235
+ return True
236
+ try:
237
+ sent = sock.send(self.__pending)
238
+ except BlockingIOError:
239
+ # The kernel buffer is full again; the tail stays queued.
240
+ return True
241
+ except OSError as error:
242
+ failure = error
243
+ else:
244
+ del self.__pending[:sent]
245
+ if failure is None:
246
+ return True
247
+ self.emit("error", failure)
248
+ return False
249
+
250
+ def __receive(self, sock: socket.socket) -> bool:
251
+ """Read one chunk. False when the connection cannot be served anymore."""
252
+ try:
253
+ data = sock.recv(_RECV_SIZE)
254
+ except BlockingIOError:
255
+ # select() woke us up for nothing; try again on the next poll.
256
+ return True
257
+ except OSError as error:
258
+ self.emit("error", error)
259
+ return False
260
+ if not data:
261
+ # An empty chunk means the server closed its end of the connection.
262
+ # The messages queued for it are still delivered before the socket
263
+ # goes, the way a half-closed connection can still be written to.
264
+ with self.__lock:
265
+ self.__closing = True
266
+ return True
267
+ self.__deliver(data)
268
+ return True
269
+
270
+ def __deliver(self, data: bytes) -> None:
271
+ """Hand the chunk over to the oldest ask() waiting and emit the event."""
272
+ with self.__lock:
273
+ waiter = self.__answers.popleft() if self.__answers else None
274
+ if waiter is not None:
275
+ waiter.put(data)
276
+ self.emit("message", data)
277
+
278
+ def __close(self, sock: socket.socket) -> None:
279
+ """Drop the connection, release the waiting ask() calls and close."""
280
+ with self.__lock:
281
+ if self.__socket is not sock:
282
+ return
283
+ self.__socket = None
284
+ self.__closing = True
285
+ self.__pending.clear()
286
+ waiters, self.__answers = list(self.__answers), deque()
287
+ for waiter in waiters:
288
+ waiter.put(None)
289
+ try:
290
+ self.emit("disconnect", self.__host, self.__port)
291
+ finally:
292
+ sock.close()
293
+
294
+ def __wait_closed(self, thread: threading.Thread) -> None:
295
+ """Wait until the serving loop has written its queue and closed."""
296
+ thread.join(_DRAIN_TIMEOUT)
297
+ if not thread.is_alive():
298
+ return
299
+ # The server stopped reading: let the queue go instead of waiting.
300
+ with self.__lock:
301
+ self.__pending.clear()
302
+ thread.join()
File without changes
@@ -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,7 @@
1
+ simple_socket_client/__init__.py,sha256=m66RyZ-aS9moaqDNWpYvZurF9Q6XP03R3cAjZO2yxAU,11566
2
+ simple_socket_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ simple_socket_client-2.0.dist-info/licenses/LICENSE.md,sha256=viUOCJf97qThnFWXx3lGwpWwhWfuo8ug8QQAHtAFh8c,1496
4
+ simple_socket_client-2.0.dist-info/METADATA,sha256=Gz7flsvCTNLwqa57j9Nh4GP186CNA9JuwWAxCXPvnxQ,10386
5
+ simple_socket_client-2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
6
+ simple_socket_client-2.0.dist-info/top_level.txt,sha256=KrID74qMjx5n9HChIxZbXInlAYezvB0-ifISpLKCtOI,21
7
+ simple_socket_client-2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2023–2026, webtoucher
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8
+
9
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
10
+
11
+ * Neither the names of Alexey Kuznetsov or webtoucher nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ simple_socket_client