pynect-worker 0.8.5__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.
- pynect_worker-0.8.5/PKG-INFO +386 -0
- pynect_worker-0.8.5/README.md +361 -0
- pynect_worker-0.8.5/pyproject.toml +74 -0
- pynect_worker-0.8.5/pyproject.toml.orig +58 -0
- pynect_worker-0.8.5/src/py/pynect_worker/__init__.py +11 -0
- pynect_worker-0.8.5/src/py/pynect_worker/__main__.py +5 -0
- pynect_worker-0.8.5/src/py/pynect_worker/_generated/__init__.py +1 -0
- pynect_worker-0.8.5/src/py/pynect_worker/_generated/metadata.py +3 -0
- pynect_worker-0.8.5/src/py/pynect_worker/bridge.py +594 -0
- pynect_worker-0.8.5/src/py/pynect_worker/cli.py +64 -0
- pynect_worker-0.8.5/src/py/pynect_worker/codegen.py +501 -0
- pynect_worker-0.8.5/src/py/pynect_worker/connection.py +147 -0
- pynect_worker-0.8.5/src/py/pynect_worker/framing.py +32 -0
- pynect_worker-0.8.5/src/py/pynect_worker/models.py +108 -0
- pynect_worker-0.8.5/src/py/pynect_worker/py.typed +1 -0
- pynect_worker-0.8.5/src/py/pynect_worker/schema.py +47 -0
- pynect_worker-0.8.5/src/py/pynect_worker/server.py +221 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pynect-worker
|
|
3
|
+
Version: 0.8.5
|
|
4
|
+
Summary: Python worker and WebSocket server for Pynect
|
|
5
|
+
Author: Reggie Bowser and NodePyX contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: cyclopts>=5.3,<6
|
|
16
|
+
Requires-Dist: msgpack>=1.1,<2
|
|
17
|
+
Requires-Dist: pydantic>=2.12,<3
|
|
18
|
+
Requires-Dist: websockets>=17,<18 ; extra == 'server'
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Project-URL: Homepage, https://github.com/regbo/lfp-js/tree/main/packages/pynect#readme
|
|
21
|
+
Project-URL: Repository, https://github.com/regbo/lfp-js.git
|
|
22
|
+
Project-URL: Issues, https://github.com/regbo/lfp-js/issues
|
|
23
|
+
Provides-Extra: server
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Pynect
|
|
27
|
+
|
|
28
|
+
Pynect gives Node.js and Bun a lazy Python environment with asynchronous proxies for Python modules, functions, classes, objects, callbacks, and iterators.
|
|
29
|
+
|
|
30
|
+
Each Venv runs in its own Python process. Separate Venvs can therefore use incompatible Python projects and dependencies in the same JavaScript application. Pynect communicates over a Unix-domain socket on Linux and macOS and a named pipe on Windows.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
bun add pynect
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Python and uv do not need to be preinstalled. Pyfig selects an existing compatible setup when possible and can install them when necessary.
|
|
39
|
+
|
|
40
|
+
## Use
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { createVenv } from 'pynect';
|
|
44
|
+
|
|
45
|
+
await using python = createVenv();
|
|
46
|
+
const math = await python.import('math');
|
|
47
|
+
|
|
48
|
+
console.log(await math.sqrt(81));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`createVenv()` is synchronous and lazy. It does not inspect Python, run uv, create a socket, or start a process until `import()` or `ready()` is called. Closing an unused Venv starts nothing.
|
|
52
|
+
|
|
53
|
+
The default project behavior matches `uv run`: uv discovers a project from the working directory and its parents. Select a project explicitly or disable project discovery when needed:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const project = createVenv({ project: './python' });
|
|
57
|
+
const isolated = createVenv({ project: false });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Additional dependencies use the corresponding uv-run concepts:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const python = createVenv({
|
|
64
|
+
project: './python',
|
|
65
|
+
with: ['httpx>=0.28', 'orjson>=3.10'],
|
|
66
|
+
withEditable: ['../shared-python'],
|
|
67
|
+
withRequirements: ['./requirements/runtime.txt'],
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
uv owns project discovery, resolution, environment synchronization, and caching. The worker stays alive for the Venv lifetime, so environment and interpreter startup happen once. Pyfig is used only to find or provision compatible uv and Python executables; Pynect does not depend on Pyfig's command runner.
|
|
72
|
+
|
|
73
|
+
## Python objects
|
|
74
|
+
|
|
75
|
+
Common values cross the bridge by value: null, booleans, numbers, strings, bytes, arrays, string-keyed objects, sets, maps, dates, dataclasses, enums, and Pydantic models. Other Python values remain remote references.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const module = await python.import('my_package');
|
|
79
|
+
const client = await module.Client('https://example.com');
|
|
80
|
+
|
|
81
|
+
console.log(await client.status);
|
|
82
|
+
console.log(await client.fetch());
|
|
83
|
+
console.log(await client.$repr());
|
|
84
|
+
console.log(await client.$dir());
|
|
85
|
+
console.log(await client.$value());
|
|
86
|
+
|
|
87
|
+
await client.$set('timeout', 10);
|
|
88
|
+
await client.$release();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Property lookup is lazy. Await a property to fetch it, or call it to invoke the Python callable. `$value()` requests a recursive by-value copy when the object supports one. Remote references are also released through JavaScript finalization and all remaining references disappear when the Venv closes.
|
|
92
|
+
|
|
93
|
+
Python keyword arguments are explicit:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { kwargs } from 'pynect';
|
|
97
|
+
|
|
98
|
+
await module.render('report', kwargs({ format: 'pdf', landscape: true }));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Python iterables and async iterables use JavaScript asynchronous iteration:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const rows = await module.stream_rows();
|
|
105
|
+
for await (const row of rows) {
|
|
106
|
+
console.log(row);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Callbacks
|
|
111
|
+
|
|
112
|
+
JavaScript functions passed as arguments become callbacks for that Venv:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const result = await module.transform([1, 2, 3], async value => {
|
|
116
|
+
return await module.normalize(value);
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Callbacks may be asynchronous and may call back into the same Python Venv. Requests carry independent IDs and the Python worker dispatches nested calls without blocking the callback-result channel.
|
|
121
|
+
|
|
122
|
+
## Pydantic
|
|
123
|
+
|
|
124
|
+
`validatePydantic` defaults to `true`. The packaged worker includes Pydantic, binds a callable's signature, and validates deserialized annotated arguments before invoking it. Pydantic models returned from Python are serialized with their configured serializers.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const python = createVenv({
|
|
128
|
+
validatePydantic: true,
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Set `validatePydantic: false` to preserve the deserialized values exactly. Pynect does not install Pydantic implicitly.
|
|
133
|
+
|
|
134
|
+
## Code generation
|
|
135
|
+
|
|
136
|
+
The Node-based `pynect codegen` command owns options, configuration, validation,
|
|
137
|
+
and editor setup. It uses Pyfig to resolve uv and Python, then sends one private
|
|
138
|
+
JSON request to the bundled Python inspection engine:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
pynect codegen generate \
|
|
142
|
+
--module name_generator \
|
|
143
|
+
--module my_project.models \
|
|
144
|
+
--class-name MyProjectPython \
|
|
145
|
+
--project . \
|
|
146
|
+
--out generated/my-project-python.ts
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A module passed without an explicit alias uses its final import segment. If names
|
|
150
|
+
conflict, the generator prepends one package parent at a time until they are
|
|
151
|
+
unique. For example, `one.shared.name_generator` and
|
|
152
|
+
`two.shared.name_generator` become `one_shared_name_generator` and
|
|
153
|
+
`two_shared_name_generator`. Use `alias=package.module` when you want to choose
|
|
154
|
+
the public name yourself.
|
|
155
|
+
|
|
156
|
+
For one module, `--class-name` is optional and is derived from its public alias:
|
|
157
|
+
`name_generator` generates `NameGeneratorPython`. Generating multiple modules
|
|
158
|
+
requires an explicit `--class-name` (or `className` in the JSON settings).
|
|
159
|
+
|
|
160
|
+
It imports the selected Python modules, inspects their public functions and
|
|
161
|
+
classes, translates annotations, and emits:
|
|
162
|
+
|
|
163
|
+
- module and class interfaces;
|
|
164
|
+
- a typed module map for `PythonVenv`;
|
|
165
|
+
- a lazy project class with typed module getters;
|
|
166
|
+
- the configured `CreateVenvOptions`, including project and uv dependency options.
|
|
167
|
+
|
|
168
|
+
### Project configuration
|
|
169
|
+
|
|
170
|
+
For a Python project, keep the generator settings in `pyproject.toml`:
|
|
171
|
+
|
|
172
|
+
```toml
|
|
173
|
+
[tool.pynect.codegen]
|
|
174
|
+
modules = ["name_generator", "my_project.models"]
|
|
175
|
+
className = "MyProjectPython"
|
|
176
|
+
out = "../node-wrapper/src/generated/my-project-python.ts"
|
|
177
|
+
project = "."
|
|
178
|
+
with = ["httpx>=0.28"]
|
|
179
|
+
withEditable = []
|
|
180
|
+
withRequirements = []
|
|
181
|
+
uvArgs = []
|
|
182
|
+
validatePydantic = true
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Then generation is one command:
|
|
186
|
+
|
|
187
|
+
```sh
|
|
188
|
+
pynect codegen generate
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`modules` may be a list using automatic aliases or an alias-to-import mapping.
|
|
192
|
+
The same fields work in a standalone JSON file:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"className": "MyProjectPython",
|
|
197
|
+
"out": "generated/my-project-python.ts",
|
|
198
|
+
"project": ".",
|
|
199
|
+
"with": ["httpx>=0.28"],
|
|
200
|
+
"modules": ["name_generator", "my_project.models"]
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
pynect codegen generate --config pynect.codegen.json
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
In `package.json`, place that same object under `pynect.codegen`. Configuration
|
|
209
|
+
precedence is CLI flags, the discovered `pyproject.toml` or `package.json`, then
|
|
210
|
+
defaults.
|
|
211
|
+
|
|
212
|
+
The generated TypeScript imports `pynect` only when the bindings are later
|
|
213
|
+
consumed by a JavaScript application. Because inspection imports the requested
|
|
214
|
+
Python modules, run the generator only against trusted projects and packages.
|
|
215
|
+
|
|
216
|
+
### Generate on save
|
|
217
|
+
|
|
218
|
+
Run setup through the binary shipped with the Node wrapper dependency:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
pynect codegen setup-dev --root . --dry-run
|
|
222
|
+
|
|
223
|
+
pynect codegen setup-dev --root .
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Setup adds `pucelle.run-on-save` to `.vscode/extensions.json` and adds a
|
|
227
|
+
debounced `pynect codegen generate` command to `.vscode/settings.json`. The setting uses
|
|
228
|
+
`runOnSave.ignoreFilesBy = [".gitignore"]`, so ignored Python files do not trigger
|
|
229
|
+
generation. [Run on Save](https://marketplace.visualstudio.com/items?itemName=pucelle.run-on-save)
|
|
230
|
+
is the recommended extension because it supports `.gitignore`, glob matching,
|
|
231
|
+
debouncing, and quiet background commands directly.
|
|
232
|
+
|
|
233
|
+
The setup command merges valid JSON files and is idempotent. It refuses to
|
|
234
|
+
rewrite JSON-with-comments because doing so would discard comments; convert those
|
|
235
|
+
workspace files to plain JSON or add the documented entries manually.
|
|
236
|
+
|
|
237
|
+
Generated code can also supply its module map directly without a wrapper class:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
interface Modules {
|
|
241
|
+
names: {
|
|
242
|
+
generate_name(): Promise<string>;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const python = createVenv<Modules>({
|
|
247
|
+
modules: { names: 'name_generator' },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const names = await python.import('names');
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Configuration
|
|
254
|
+
|
|
255
|
+
Important options are:
|
|
256
|
+
|
|
257
|
+
| Option | Default | Purpose |
|
|
258
|
+
| --- | --- | --- |
|
|
259
|
+
| `cwd` | `process.cwd()` | uv working directory and first application import path |
|
|
260
|
+
| `project` | uv discovery | project directory, or `false` for `--no-project` |
|
|
261
|
+
| `with` | `[]` | additional uv requirements |
|
|
262
|
+
| `withEditable` | `[]` | additional editable requirements |
|
|
263
|
+
| `withRequirements` | `[]` | requirements, pylock, or inline-metadata files |
|
|
264
|
+
| `modules` | `{}` | public aliases mapped to Python import names |
|
|
265
|
+
| `validatePydantic` | `true` | validate annotated inputs when Pydantic is available |
|
|
266
|
+
| `autoClose` | `true` | allow natural host exit and terminate the child on shutdown |
|
|
267
|
+
| `startupTimeoutMs` | `120000` | environment preparation and worker handshake limit |
|
|
268
|
+
| `callTimeoutMs` | `0` | per-call RPC limit; zero disables it |
|
|
269
|
+
| `uvArgs` | `[]` | additional `uv run` flags before the worker command |
|
|
270
|
+
|
|
271
|
+
Python and uv selection options such as `python`, `uv`, and `debug` are passed to Pyfig.
|
|
272
|
+
|
|
273
|
+
## WebSocket transport
|
|
274
|
+
|
|
275
|
+
The separately installable `pynect-worker` Python distribution serves the same
|
|
276
|
+
protocol directly. Its `server` extra installs WebSocket support, while the
|
|
277
|
+
package remains compatible with Python 3.11:
|
|
278
|
+
|
|
279
|
+
```sh
|
|
280
|
+
uvx --from 'pynect-worker[server]' pynect-server \
|
|
281
|
+
--host 127.0.0.1 \
|
|
282
|
+
--port 8000 \
|
|
283
|
+
--path /pynect \
|
|
284
|
+
--modules.names name_generator
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`pynect-worker serve` exposes the same command. The Python CLI is built from the
|
|
288
|
+
canonical Pydantic `ServerOptions` model with Cyclopts, so its help, validation,
|
|
289
|
+
JSON Schema, and JavaScript configuration use the same field definitions.
|
|
290
|
+
|
|
291
|
+
The browser-compatible client entrypoint contains no Node.js, uv, or local IPC
|
|
292
|
+
imports:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { connectVenv } from 'pynect/websocket';
|
|
296
|
+
|
|
297
|
+
await using python = connectVenv({
|
|
298
|
+
url: 'wss://app.example.com/pynect',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const names = await python.import('names');
|
|
302
|
+
console.log(await names.generate_name());
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The proxy must support WebSocket upgrades, preserve the `pynect.v2` subprotocol,
|
|
306
|
+
and preserve binary messages. Keep the listener private unless an equivalent
|
|
307
|
+
network boundary is provided; authentication and TLS deliberately belong to the
|
|
308
|
+
proxy.
|
|
309
|
+
|
|
310
|
+
One Python server process handles multiple clients. Every connection gets an
|
|
311
|
+
isolated object registry and callback state, but not another Python process or
|
|
312
|
+
uv environment. A WebSocket disconnect closes only that session and never
|
|
313
|
+
forwards a signal. SIGINT or SIGTERM stops the server as a whole; a termination
|
|
314
|
+
request received during startup is latched and replayed after the listener is
|
|
315
|
+
created.
|
|
316
|
+
|
|
317
|
+
Only names in the server's `modules` map can be imported. The map is empty by
|
|
318
|
+
default, so a new server exposes nothing. Keys are the public names clients use
|
|
319
|
+
and values are import names or Python file paths.
|
|
320
|
+
|
|
321
|
+
Server options are `host`, `port`, `path`, `modules`, `maxFrameBytes`, and
|
|
322
|
+
`validatePydantic`. Client options are `url`, `maxFrameBytes`,
|
|
323
|
+
`startupTimeoutMs`, `callTimeoutMs`, and `autoClose`.
|
|
324
|
+
|
|
325
|
+
## Python package
|
|
326
|
+
|
|
327
|
+
Projen synthesizes `pyproject.toml` beside the npm manifest and uv builds both a
|
|
328
|
+
wheel and source archive with `uv_build`. JavaScript sources live under
|
|
329
|
+
`src/js`; Python sources live under `src/py`. The npm archive includes the exact
|
|
330
|
+
worker wheel under `dist/py`, so local IPC installs that wheel into the selected
|
|
331
|
+
uv environment instead of copying private scripts.
|
|
332
|
+
|
|
333
|
+
The Pydantic models in `pynect_worker.models` are the source of truth for shared
|
|
334
|
+
worker and server options. `src/js/generate-schema.ts` emits tracked JSON Schema,
|
|
335
|
+
TypeScript interfaces, Zod adapters, and protocol constants. CI checks those
|
|
336
|
+
generated files for drift. JavaScript tools can import the schemas and generated
|
|
337
|
+
types from `pynect/schema`.
|
|
338
|
+
|
|
339
|
+
## Transport and lifecycle
|
|
340
|
+
|
|
341
|
+
Messages are length-prefixed MessagePack frames. One connection multiplexes calls, responses, callback requests, releases, and shutdown. The WebSocket transport preserves those frames as binary messages and defaults to an 8 MiB frame limit. Windows local IPC uses overlapped named-pipe reads and writes so a blocked read does not prevent a callback response or nested call. Python stdout and stderr remain attached to the host rather than carrying protocol traffic.
|
|
342
|
+
|
|
343
|
+
Remote objects, completed iterators, and JavaScript callback leases are released
|
|
344
|
+
as soon as their cross-runtime ownership ends. Explicit `$release()` and
|
|
345
|
+
`await using` remain available for deterministic object cleanup.
|
|
346
|
+
|
|
347
|
+
Use `await using`, call `close()` explicitly, or rely on `autoClose` for process shutdown:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const python = createVenv({ autoClose: false });
|
|
351
|
+
try {
|
|
352
|
+
const module = await python.import('my_package');
|
|
353
|
+
await module.run();
|
|
354
|
+
} finally {
|
|
355
|
+
await python.close();
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
With `autoClose`, natural host exit waits for every active uv and Python process.
|
|
360
|
+
Catchable termination signals are forwarded before that wait: `SIGHUP`, `SIGINT`,
|
|
361
|
+
`SIGQUIT`, and `SIGTERM` on Unix, plus `SIGBREAK` on Windows. The uncatchable
|
|
362
|
+
`SIGKILL` and `SIGSTOP` signals cannot run asynchronous cleanup. Signal handling
|
|
363
|
+
starts before uv or Python resolution, so one termination signal either cancels
|
|
364
|
+
pre-spawn work or is forwarded once to a child that is already starting; it does
|
|
365
|
+
not require another signal after the Python handshake.
|
|
366
|
+
|
|
367
|
+
## Development
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
bun install --frozen-lockfile --ignore-scripts
|
|
371
|
+
uv sync --locked
|
|
372
|
+
bun run verify
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Run the opt-in I/O smoke benchmarks separately:
|
|
376
|
+
|
|
377
|
+
```sh
|
|
378
|
+
bun run test:smoke
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The smoke suite compares first and warm operations against pure Python launched
|
|
382
|
+
through `uv run`. Runtime, process, server, and fixture setup finish before the
|
|
383
|
+
timers start. HTTP checks use `localhost` and `127.0.0.1` loopback endpoints so
|
|
384
|
+
external network latency and availability do not affect results.
|
|
385
|
+
|
|
386
|
+
MIT licensed.
|