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.
@@ -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.