shtoobz 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.
- shtoobz-0.1.0/.gitlab-ci.yml +67 -0
- shtoobz-0.1.0/LICENSE +21 -0
- shtoobz-0.1.0/PKG-INFO +405 -0
- shtoobz-0.1.0/README.md +381 -0
- shtoobz-0.1.0/pyproject.toml +44 -0
- shtoobz-0.1.0/src/shtoobz/__init__.py +11 -0
- shtoobz-0.1.0/src/shtoobz/exceptions.py +32 -0
- shtoobz-0.1.0/src/shtoobz/process.py +182 -0
- shtoobz-0.1.0/src/shtoobz/py.typed +0 -0
- shtoobz-0.1.0/src/shtoobz/shtoob.py +379 -0
- shtoobz-0.1.0/src/shtoobz/socket.py +65 -0
- shtoobz-0.1.0/tests/check_distribution.py +40 -0
- shtoobz-0.1.0/tests/check_release.py +47 -0
- shtoobz-0.1.0/tests/peer.py +63 -0
- shtoobz-0.1.0/tests/test_release.py +78 -0
- shtoobz-0.1.0/tests/test_shtoob.py +631 -0
- shtoobz-0.1.0/tests/typing_api.py +46 -0
- shtoobz-0.1.0/uv.lock +289 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
stages:
|
|
2
|
+
- verify
|
|
3
|
+
- publish
|
|
4
|
+
|
|
5
|
+
variables:
|
|
6
|
+
UV_CACHE_DIR: .uv-cache
|
|
7
|
+
UV_LINK_MODE: copy
|
|
8
|
+
|
|
9
|
+
.checks:
|
|
10
|
+
stage: verify
|
|
11
|
+
timeout: 20m
|
|
12
|
+
interruptible: true
|
|
13
|
+
parallel:
|
|
14
|
+
matrix:
|
|
15
|
+
- PYTHON_VERSION: ["3.11", "3.14"]
|
|
16
|
+
before_script:
|
|
17
|
+
- python -m pip install --disable-pip-version-check "uv==0.9.13"
|
|
18
|
+
artifacts:
|
|
19
|
+
name: "$CI_JOB_NAME_SLUG"
|
|
20
|
+
paths:
|
|
21
|
+
- dist/
|
|
22
|
+
expire_in: 1 week
|
|
23
|
+
|
|
24
|
+
linux:
|
|
25
|
+
extends: .checks
|
|
26
|
+
image: "python:${PYTHON_VERSION}-slim"
|
|
27
|
+
script:
|
|
28
|
+
- python -m uv sync --locked --python "$PYTHON_VERSION"
|
|
29
|
+
- python -m uv run --locked mypy
|
|
30
|
+
- python -m uv build --no-sources
|
|
31
|
+
- python -m uv run --no-cache --isolated --no-project --python "$PYTHON_VERSION" --with ./dist/*.whl python tests/check_distribution.py
|
|
32
|
+
- python -m uv run --no-cache --isolated --no-project --python "$PYTHON_VERSION" --with ./dist/*.whl python -W error::ResourceWarning -m unittest discover -s tests -q
|
|
33
|
+
|
|
34
|
+
windows:
|
|
35
|
+
extends: .checks
|
|
36
|
+
tags:
|
|
37
|
+
- saas-windows-medium-amd64
|
|
38
|
+
script:
|
|
39
|
+
- python -m uv sync --locked --python $env:PYTHON_VERSION
|
|
40
|
+
- python -m uv run --locked mypy
|
|
41
|
+
- python -m uv build --no-sources
|
|
42
|
+
- $wheel = (Get-ChildItem dist/*.whl).FullName
|
|
43
|
+
- python -m uv run --no-cache --isolated --no-project --python $env:PYTHON_VERSION --with $wheel python tests/check_distribution.py
|
|
44
|
+
- python -m uv run --no-cache --isolated --no-project --python $env:PYTHON_VERSION --with $wheel python -W error::ResourceWarning -m unittest discover -s tests -q
|
|
45
|
+
|
|
46
|
+
# Publish the exact artifacts already tested by linux: [3.11]. Stage ordering
|
|
47
|
+
# also requires every other Windows/Linux verification job to pass first.
|
|
48
|
+
publish:
|
|
49
|
+
stage: publish
|
|
50
|
+
image: python:3.11-slim
|
|
51
|
+
timeout: 10m
|
|
52
|
+
interruptible: false
|
|
53
|
+
dependencies:
|
|
54
|
+
- "linux: [3.11]"
|
|
55
|
+
rules:
|
|
56
|
+
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ && $CI_PROJECT_PATH == "stolenfootball-tools/shtoobz"'
|
|
57
|
+
environment:
|
|
58
|
+
name: pypi
|
|
59
|
+
url: https://pypi.org/project/shtoobz/
|
|
60
|
+
id_tokens:
|
|
61
|
+
PYPI_ID_TOKEN:
|
|
62
|
+
aud: pypi
|
|
63
|
+
before_script:
|
|
64
|
+
- python -m pip install --disable-pip-version-check "uv==0.9.13"
|
|
65
|
+
script:
|
|
66
|
+
- python tests/check_release.py "$CI_COMMIT_TAG"
|
|
67
|
+
- python -m uv publish --trusted-publishing always --publish-url https://upload.pypi.org/legacy/ --check-url https://pypi.org/simple/ dist/*.whl dist/*.tar.gz
|
shtoobz-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeremy Dunn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
shtoobz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shtoobz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lightweight interchangeable process and TCP byte streams for Windows and Linux.
|
|
5
|
+
Author: Jeremy Dunn
|
|
6
|
+
Author-email: Jeremy Dunn <jeremy.dunn315@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Project-URL: Documentation, https://gitlab.com/stolenfootball-tools/shtoobz/-/blob/main/README.md
|
|
21
|
+
Project-URL: Issues, https://gitlab.com/stolenfootball-tools/shtoobz/-/issues
|
|
22
|
+
Project-URL: Repository, https://gitlab.com/stolenfootball-tools/shtoobz
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# shtoobz
|
|
26
|
+
|
|
27
|
+
Lightweight, interchangeable byte streams for Windows and Linux. A **shtoob**
|
|
28
|
+
talks to a child process over stdin/stdout or to an application over TCP. The
|
|
29
|
+
client's communication code stays the same when you switch transports.
|
|
30
|
+
|
|
31
|
+
Python 3.11+; no third-party runtime dependencies. Managed and built with uv.
|
|
32
|
+
|
|
33
|
+
## Development
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
uv sync --locked
|
|
37
|
+
uv run python -m unittest discover -s tests -v
|
|
38
|
+
uv run mypy
|
|
39
|
+
uv build
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The build produces a wheel and source archive in `dist/`. To use the checkout
|
|
43
|
+
from another uv project, run `uv add /path/to/shtoobz`. No published package is
|
|
44
|
+
required.
|
|
45
|
+
|
|
46
|
+
GitLab CI verifies Windows and Linux on Python 3.11 and 3.14. Each job runs
|
|
47
|
+
strict type checks, builds the distributions, validates their metadata and
|
|
48
|
+
contents, and runs the integration suite against an isolated wheel installation.
|
|
49
|
+
The Windows jobs use GitLab's `saas-windows-medium-amd64` hosted runner tag; enable
|
|
50
|
+
hosted runners for the project (or substitute your own Windows runner tag).
|
|
51
|
+
Branch pipelines save build artifacts. A matching `vX.Y.Z` tag in the canonical
|
|
52
|
+
GitLab project publishes to PyPI after all verification jobs pass; see the release
|
|
53
|
+
steps below.
|
|
54
|
+
|
|
55
|
+
The source archive includes the tests, lockfile, CI configuration, and MIT
|
|
56
|
+
license. The installed library has no runtime dependencies; mypy is a development
|
|
57
|
+
dependency only.
|
|
58
|
+
|
|
59
|
+
## Switch transports
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from shtoobz import ProcessShtoob, SocketShtoob
|
|
63
|
+
|
|
64
|
+
LOCAL = True
|
|
65
|
+
|
|
66
|
+
shtoob = (
|
|
67
|
+
ProcessShtoob(["./example", "--stdio"], timeout=5)
|
|
68
|
+
if LOCAL
|
|
69
|
+
else SocketShtoob("127.0.0.1", 9000, timeout=5)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
with shtoob:
|
|
73
|
+
shtoob.recvuntil(b"Name: ")
|
|
74
|
+
shtoob.sendline(b"Alice")
|
|
75
|
+
print(shtoob.recvline())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use the appropriate executable path on Windows, for example `r".\example.exe"`.
|
|
79
|
+
Both endpoints must expose the same byte protocol; shtoobz does not translate
|
|
80
|
+
between a CLI menu and a different network protocol.
|
|
81
|
+
|
|
82
|
+
A runnable process example on both platforms:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import sys
|
|
86
|
+
from shtoobz import ProcessShtoob
|
|
87
|
+
|
|
88
|
+
with ProcessShtoob(
|
|
89
|
+
[sys.executable, "-u", "-c", "print(input().upper(), flush=True)"],
|
|
90
|
+
timeout=5,
|
|
91
|
+
) as shtoob:
|
|
92
|
+
shtoob.sendline(b"hello")
|
|
93
|
+
assert shtoob.recvline().rstrip(b"\r\n") == b"HELLO"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Inject a shtoob into your application instead of constructing a socket inside it:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from shtoobz import Shtoob
|
|
100
|
+
|
|
101
|
+
class Client:
|
|
102
|
+
def __init__(self, shtoob: Shtoob):
|
|
103
|
+
self.shtoob = shtoob
|
|
104
|
+
|
|
105
|
+
def request(self, payload: bytes) -> bytes:
|
|
106
|
+
self.shtoob.sendline(payload)
|
|
107
|
+
return self.shtoob.recvline()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Communication API
|
|
111
|
+
|
|
112
|
+
All payloads and delimiters are `bytes`. Encode/decode text explicitly.
|
|
113
|
+
|
|
114
|
+
| Method | Behavior |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `recv(n=4096)` | Read up to `n` bytes, waiting only if no data is available. Return `b""` at EOF. |
|
|
117
|
+
| `recvn(n)` | Read exactly `n` bytes or raise `ShtoobEOF`/`ShtoobTimeout`. |
|
|
118
|
+
| `peek(n=4096)` | Inspect up to `n` bytes without consuming them; wait only if the buffer is empty. |
|
|
119
|
+
| `unrecv(data)` | Prepend bytes to the receive buffer. |
|
|
120
|
+
| `recvuntil(delimiter)` | Read through and include the delimiter. |
|
|
121
|
+
| `recvany(*delimiters)` | Read through the earliest completed delimiter (the shortest matching prefix). |
|
|
122
|
+
| `recvline()` | Read through and include the configured newline. |
|
|
123
|
+
| `recvall()` | Read until EOF; a short read or idle period is not EOF. |
|
|
124
|
+
| `recv_available()` | Collect output until an idle gap or EOF. |
|
|
125
|
+
| `iterlines()` | Yield lines and any unterminated final line. |
|
|
126
|
+
| `iterchunks(size=4096)` | Yield chunks of at most `size` bytes until EOF. |
|
|
127
|
+
| `sendall(data)` | Write all bytes. |
|
|
128
|
+
| `sendline(data=b"")` | Write data followed by the configured newline. |
|
|
129
|
+
| `sendafter(delimiter, data)` | Receive a prompt, send bytes, and return the prompt. |
|
|
130
|
+
| `sendlineafter(delimiter, data=b"")` | Receive a prompt, send a line, and return the prompt. |
|
|
131
|
+
| `send_eof()` | Close the sending direction while keeping responses readable. |
|
|
132
|
+
| `close()` | Release resources; repeated calls are harmless. |
|
|
133
|
+
| `closed` | Whether this shtoob has been closed locally. |
|
|
134
|
+
| `write_closed` | Whether the sending direction has been closed locally. |
|
|
135
|
+
|
|
136
|
+
Every receive method accepts a keyword `timeout`. If omitted, the constructor's
|
|
137
|
+
timeout applies. The default is `None` (wait indefinitely); an explicit `None`
|
|
138
|
+
overrides a finite default. Zero attempts an immediate poll. A finite timeout is
|
|
139
|
+
a deadline for the read operation, not an idle timeout reset by every chunk.
|
|
140
|
+
Writes are blocking and do not have a timeout.
|
|
141
|
+
|
|
142
|
+
Both `recv(0)` and `recvn(0)` return `b""`. Delimiters must be nonempty; negative
|
|
143
|
+
counts/timeouts and nonfinite timeouts are rejected. Delimiters are included in
|
|
144
|
+
results, and bytes received after a delimiter are retained for the next read.
|
|
145
|
+
|
|
146
|
+
On timeout, bytes already received remain buffered. On EOF, an incomplete exact
|
|
147
|
+
or delimiter read raises `ShtoobEOF`; its `partial` attribute contains a snapshot
|
|
148
|
+
of retained data. Retrieve that data with `recv()` or `recvall()`. `recvline()`
|
|
149
|
+
therefore raises on an unterminated final line rather than returning it silently.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from shtoobz import ShtoobTimeout
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
reply = shtoob.recvuntil(b"> ", timeout=0.5)
|
|
156
|
+
except ShtoobTimeout:
|
|
157
|
+
# Continue the same response; previously received bytes have not been lost.
|
|
158
|
+
reply = shtoob.recvuntil(b"> ", timeout=5)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`ShtoobTimeout` also inherits `TimeoutError`; `ShtoobEOF` also inherits `EOFError`.
|
|
162
|
+
Operations after local closure raise `ShtoobClosed`. These all inherit
|
|
163
|
+
`ShtoobError`. Other OS failures (connection refused/reset, executable not found,
|
|
164
|
+
permissions, etc.) retain their standard Python exceptions. A failed write may
|
|
165
|
+
have sent a prefix; do not retry the entire payload blindly.
|
|
166
|
+
|
|
167
|
+
## Finish input without closing output
|
|
168
|
+
|
|
169
|
+
Some programs do not produce output until their input stream reaches EOF.
|
|
170
|
+
`send_eof()` closes a child's stdin or performs a TCP write shutdown, allowing
|
|
171
|
+
the same pattern for either transport:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
shtoob.sendall(payload)
|
|
175
|
+
shtoob.send_eof()
|
|
176
|
+
result = shtoob.recvall(timeout=5, max_bytes=1024 * 1024)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Repeated `send_eof()` calls are harmless while the shtoob remains open. Further
|
|
180
|
+
writes (including empty writes) raise `ShtoobEOF`; reads continue normally.
|
|
181
|
+
After `close()`, `send_eof()` raises `ShtoobClosed`. Half-closing does not terminate
|
|
182
|
+
a child process. It waits for any ongoing write, so it is not a way to interrupt
|
|
183
|
+
a blocked writer.
|
|
184
|
+
|
|
185
|
+
## Limit accumulated responses
|
|
186
|
+
|
|
187
|
+
`recvuntil`, `recvany`, `recvline`, and `recvall` accept `max_bytes=None` by default.
|
|
188
|
+
Pass a nonnegative integer to bound the response, including its delimiter:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from shtoobz import ShtoobLimitExceeded
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
line = shtoob.recvline(timeout=5, max_bytes=4096)
|
|
195
|
+
except ShtoobLimitExceeded as exc:
|
|
196
|
+
print(f"Response did not fit within {exc.max_bytes} bytes")
|
|
197
|
+
# Received bytes remain available; choose whether to read or close.
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
A delimiter ending exactly at the limit succeeds. If the limit is reached
|
|
201
|
+
without a complete delimiter, `ShtoobLimitExceeded` is raised immediately. A
|
|
202
|
+
delimiter read with `max_bytes=0` therefore always raises. For `recvall`, exactly
|
|
203
|
+
the limit is allowed if followed by EOF; one extra byte may be read to distinguish
|
|
204
|
+
EOF from overflow. That lookahead byte is retained if the operation fails.
|
|
205
|
+
`recvall(max_bytes=0)` accepts only an empty stream followed by EOF.
|
|
206
|
+
|
|
207
|
+
Limits apply to the entire requested response, including data buffered by earlier
|
|
208
|
+
operations. They never discard previously buffered data. The process reader's
|
|
209
|
+
separate bounded buffer and OS buffers can still contain additional bytes.
|
|
210
|
+
|
|
211
|
+
## Convenience methods
|
|
212
|
+
|
|
213
|
+
Prompt helpers return the consumed prompt, including its delimiter:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
prompt = shtoob.sendlineafter(b"Name: ", b"Alice", timeout=5, max_bytes=4096)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`sendafter()` sends the supplied bytes unchanged; `sendlineafter()` appends the
|
|
220
|
+
configured newline. Their timeout and byte limit apply to reading the prompt.
|
|
221
|
+
Writes remain blocking. A failed prompt read sends nothing and preserves partial
|
|
222
|
+
input. These helpers compose a read and write; they are not atomic transactions
|
|
223
|
+
against other readers or writers.
|
|
224
|
+
|
|
225
|
+
`peek(n, timeout=...)` behaves like `recv(n)` but leaves the result in the buffer.
|
|
226
|
+
It can return fewer than `n` bytes. `unrecv(data)` prepends bytes, so successive
|
|
227
|
+
calls put the most recently supplied bytes first. Both work after peer EOF until
|
|
228
|
+
local closure; `unrecv()` does not reopen the underlying stream. Neither changes
|
|
229
|
+
the bytes that have been sent to the application.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
prefix = shtoob.peek(4)
|
|
233
|
+
header = shtoob.recvn(4)
|
|
234
|
+
shtoob.unrecv(header) # Let another parser consume the complete header.
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`recv_available(idle_timeout=0.05, timeout=..., max_bytes=None)` collects output
|
|
238
|
+
until no new bytes arrive for `idle_timeout` seconds or EOF occurs. It returns
|
|
239
|
+
`b""` if that idle period passes without data. Use `idle_timeout=0` to drain only
|
|
240
|
+
bytes already available; a live stream may produce more immediately afterward.
|
|
241
|
+
The independent `timeout` is an overall deadline using the constructor's default.
|
|
242
|
+
It raises `ShtoobTimeout` and preserves all data if output keeps arriving beyond
|
|
243
|
+
that deadline. `max_bytes` behaves like `recvall`, including its one-byte
|
|
244
|
+
lookahead. An idle gap is not proof of a complete application message; use a
|
|
245
|
+
delimiter or known length when the protocol provides one.
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
output = shtoob.recv_available(idle_timeout=0.1, timeout=2, max_bytes=65536)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`iterlines(timeout=..., max_bytes=None)` includes each line's newline and yields
|
|
252
|
+
an unterminated final line at EOF. Its timeout and byte limit apply to each line.
|
|
253
|
+
`iterchunks(size=4096, timeout=...)` yields nonempty chunks up to the requested
|
|
254
|
+
positive size, with a timeout per chunk. A timeout or limit error ends that
|
|
255
|
+
iterator while leaving incomplete input buffered. Neither iterator holds a read
|
|
256
|
+
lock between yields, and stopping early leaves the shtoob open and remaining
|
|
257
|
+
bytes available. Use one logical consumer at a time.
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
for chunk in shtoob.iterchunks(8192, timeout=5):
|
|
261
|
+
output_file.write(chunk)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Opt-in traffic logging
|
|
265
|
+
|
|
266
|
+
Pass a standard-library `logging.Logger` to either constructor. Without one,
|
|
267
|
+
shtoobz emits no traffic logs, even if the root logger has DEBUG enabled.
|
|
268
|
+
It never installs handlers or changes logging levels itself:
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
import logging
|
|
272
|
+
from shtoobz import SocketShtoob
|
|
273
|
+
|
|
274
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
275
|
+
with SocketShtoob("localhost", 9000, logger=logging.getLogger("myapp.shtoobz")) as shtoob:
|
|
276
|
+
shtoob.sendline(b"hello")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
DEBUG records include the backend class, direction (`send` or `recv`), byte count,
|
|
280
|
+
and the complete payload as a bytes representation. Receive records describe raw
|
|
281
|
+
chunks entering the shared buffer; peeking, replaying buffered bytes, and
|
|
282
|
+
`unrecv()` do not duplicate them. Send records describe successfully completed
|
|
283
|
+
writes. A failed write may have sent a prefix that is not logged. Exceptions from
|
|
284
|
+
logging handlers are ignored so diagnostics cannot change successful I/O into an
|
|
285
|
+
apparent transport failure.
|
|
286
|
+
|
|
287
|
+
## Process options and lifecycle
|
|
288
|
+
|
|
289
|
+
`ProcessShtoob(argv, *, cwd=None, env=None, stderr="inherit", timeout=None,
|
|
290
|
+
newline=b"\n", buffer_size=65536, logger=None)` launches immediately with `shell=False`.
|
|
291
|
+
Pass an argument list, not a shell command string. `env` replaces the child's
|
|
292
|
+
environment; use `{**os.environ, "KEY": "value"}` to extend it.
|
|
293
|
+
|
|
294
|
+
- `stderr="inherit"` keeps diagnostics out of the protocol stream (default).
|
|
295
|
+
- `stderr="merge"` combines stderr with stdout.
|
|
296
|
+
- `stderr="discard"` ignores stderr.
|
|
297
|
+
- `newline=b"\r\n"` configures both `sendline()` and `recvline()` for CRLF.
|
|
298
|
+
- `pid` and `returncode` expose process status; `returncode` is `None` while running.
|
|
299
|
+
- `wait(timeout=...)` waits for normal exit and returns the process exit code.
|
|
300
|
+
|
|
301
|
+
`wait()` follows the constructor's timeout by default; `wait(None)` waits
|
|
302
|
+
indefinitely and `wait(0)` polls. Timeout raises `ShtoobTimeout` without terminating
|
|
303
|
+
the child. Waiting neither consumes output nor closes the shtoob, and an exited
|
|
304
|
+
child's status can also be retrieved after `close()`.
|
|
305
|
+
|
|
306
|
+
The process reader retains its bounded buffer during `wait()`. A child producing
|
|
307
|
+
more output than its buffers can hold may block before it exits. Drain output
|
|
308
|
+
before waiting, or read concurrently; use a finite timeout when output volume is
|
|
309
|
+
unknown. A typical finite-output flow is:
|
|
310
|
+
|
|
311
|
+
```python
|
|
312
|
+
shtoob.sendall(payload)
|
|
313
|
+
shtoob.send_eof()
|
|
314
|
+
output = shtoob.recvall(timeout=5, max_bytes=1024 * 1024)
|
|
315
|
+
exit_code = shtoob.wait(timeout=5) # ProcessShtoob only.
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
A background reader uses a bounded buffer and backpressure for portable timed
|
|
319
|
+
pipe reads. An individual read still accumulates its response in memory; use
|
|
320
|
+
`max_bytes` or read in chunks when handling large or unbounded output.
|
|
321
|
+
The child must flush output promptly (for Python children, use `-u`). Bytes are
|
|
322
|
+
not decoded or newline-translated by shtoobz.
|
|
323
|
+
|
|
324
|
+
Use a context manager or call `close()` explicitly. Closing terminates the direct
|
|
325
|
+
child if still running, waits up to one second, then kills it if necessary and
|
|
326
|
+
reaps it. It does not terminate descendant processes. Avoid children that leave
|
|
327
|
+
descendants holding the pipe handles open. A normal EOF does not automatically
|
|
328
|
+
close the shtoob. Windows console children are launched without a new window.
|
|
329
|
+
|
|
330
|
+
## Socket options
|
|
331
|
+
|
|
332
|
+
`SocketShtoob(host, port, *, timeout=None, connect_timeout=10, newline=b"\n", logger=None)`
|
|
333
|
+
connects immediately to an IPv4 or IPv6 TCP endpoint. `connect_timeout` is passed
|
|
334
|
+
to Python's connection helper; DNS resolution is subject to the operating
|
|
335
|
+
system's resolver behavior. `close()` shuts down and closes the socket.
|
|
336
|
+
|
|
337
|
+
## Scope
|
|
338
|
+
|
|
339
|
+
One reader and one writer can operate concurrently. Reads are serialized, as are
|
|
340
|
+
writes; use one logical consumer so responses are not split between callers.
|
|
341
|
+
`close()` wakes blocked readers. No global signal handlers are installed.
|
|
342
|
+
|
|
343
|
+
This first version supports redirected process pipes and TCP. PTY/ConPTY terminal
|
|
344
|
+
emulation, interactive terminal forwarding, async APIs, TLS, server listeners,
|
|
345
|
+
and process-tree management are outside its scope.
|
|
346
|
+
|
|
347
|
+
When migrating from the original wrappers, change exact `recv(n)` calls to
|
|
348
|
+
`recvn(n)`, pass the target to the constructor, and call `shtoob.close()` instead
|
|
349
|
+
of accessing an underlying socket.
|
|
350
|
+
|
|
351
|
+
Licensed under the [MIT License](LICENSE). Copyright 2026 Jeremy Dunn.
|
|
352
|
+
|
|
353
|
+
## Publishing to PyPI
|
|
354
|
+
|
|
355
|
+
Publishing uses GitLab OIDC trusted publishing, following the same approach as
|
|
356
|
+
ropinator. No PyPI API token is needed. The `publish` job uses environment `pypi`
|
|
357
|
+
and only runs for stable version tags (`vX.Y.Z`) in `stolenfootball-tools/shtoobz`.
|
|
358
|
+
All four Windows/Linux verification jobs must pass first. It uploads the exact
|
|
359
|
+
wheel and source archive tested by `linux: [3.11]`, without rebuilding them.
|
|
360
|
+
The release guard rejects a tag/version mismatch, extra or missing files, and
|
|
361
|
+
distribution metadata that does not match `pyproject.toml`.
|
|
362
|
+
|
|
363
|
+
### One-time PyPI setup (web UI)
|
|
364
|
+
|
|
365
|
+
1. Sign in to [PyPI](https://pypi.org/), using an account with a verified email
|
|
366
|
+
and two-factor authentication enabled.
|
|
367
|
+
2. Open [Account settings > Publishing](https://pypi.org/manage/account/publishing/).
|
|
368
|
+
3. Under **Add a new pending publisher**, select **GitLab** and enter:
|
|
369
|
+
|
|
370
|
+
| Field | Value |
|
|
371
|
+
| --- | --- |
|
|
372
|
+
| PyPI Project Name | `shtoobz` |
|
|
373
|
+
| Namespace | `stolenfootball-tools` |
|
|
374
|
+
| Project name | `shtoobz` |
|
|
375
|
+
| Top-level pipeline file path | `.gitlab-ci.yml` |
|
|
376
|
+
| Environment name | `pypi` |
|
|
377
|
+
|
|
378
|
+
4. Click **Add**. The publisher stays pending until the first successful upload,
|
|
379
|
+
which creates the PyPI project. A pending publisher does not reserve the name.
|
|
380
|
+
|
|
381
|
+
For an existing PyPI project you own, use that project's **Manage > Publishing**
|
|
382
|
+
page instead of adding a pending publisher.
|
|
383
|
+
|
|
384
|
+
### Publish version 0.1.0 (web UI)
|
|
385
|
+
|
|
386
|
+
1. Confirm the latest `main` pipeline is green under
|
|
387
|
+
[Build > Pipelines](https://gitlab.com/stolenfootball-tools/shtoobz/-/pipelines).
|
|
388
|
+
2. Open [Code > Tags](https://gitlab.com/stolenfootball-tools/shtoobz/-/tags) and
|
|
389
|
+
select **New tag**.
|
|
390
|
+
3. Set **Tag name** to `v0.1.0` and **Create from** to `main` (or the exact verified
|
|
391
|
+
release commit). Optionally enter `Release 0.1.0` as the message.
|
|
392
|
+
4. Select **Create tag**. This is the action that starts the publishing pipeline.
|
|
393
|
+
5. Open the tag's pipeline. After the four verification jobs pass, **publish**
|
|
394
|
+
uploads the files automatically. No separate manual job or GitLab release is
|
|
395
|
+
required. The `pypi` environment is created by that job automatically.
|
|
396
|
+
6. Verify [shtoobz on PyPI](https://pypi.org/project/shtoobz/) shows version `0.1.0`.
|
|
397
|
+
|
|
398
|
+
For subsequent releases, update the project version with `uv version`, commit the
|
|
399
|
+
updated metadata/lockfile, push, and create the corresponding `vX.Y.Z` tag once CI
|
|
400
|
+
passes. Retry a failed publish job only with the same tested artifacts; the upload
|
|
401
|
+
checks PyPI for already-uploaded identical files. Use a new version for changed
|
|
402
|
+
release files.
|
|
403
|
+
|
|
404
|
+
References: [PyPI trusted publisher setup](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/),
|
|
405
|
+
[GitLab tag creation](https://docs.gitlab.com/user/project/repository/tags/#from-the-ui).
|