pynect 0.0.0-stage → 0.2.0

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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +478 -2
  3. package/SECURITY.md +34 -0
  4. package/binding.gyp +60 -0
  5. package/dist/cli/codegen.d.ts +3 -0
  6. package/dist/cli/codegen.d.ts.map +1 -0
  7. package/dist/cli/codegen.js +543 -0
  8. package/dist/cli/codegen.js.map +1 -0
  9. package/dist/cli/sync.d.ts +3 -0
  10. package/dist/cli/sync.d.ts.map +1 -0
  11. package/dist/cli/sync.js +48 -0
  12. package/dist/cli/sync.js.map +1 -0
  13. package/dist/config/project.d.ts +57 -0
  14. package/dist/config/project.d.ts.map +1 -0
  15. package/dist/config/project.js +176 -0
  16. package/dist/config/project.js.map +1 -0
  17. package/dist/core/IsolatedWorker.d.ts +39 -0
  18. package/dist/core/IsolatedWorker.d.ts.map +1 -0
  19. package/dist/core/IsolatedWorker.js +178 -0
  20. package/dist/core/IsolatedWorker.js.map +1 -0
  21. package/dist/core/PyCallable.d.ts +67 -0
  22. package/dist/core/PyCallable.d.ts.map +1 -0
  23. package/dist/core/PyCallable.js +104 -0
  24. package/dist/core/PyCallable.js.map +1 -0
  25. package/dist/core/PyContext.d.ts +76 -0
  26. package/dist/core/PyContext.d.ts.map +1 -0
  27. package/dist/core/PyContext.js +239 -0
  28. package/dist/core/PyContext.js.map +1 -0
  29. package/dist/core/PyIterator.d.ts +84 -0
  30. package/dist/core/PyIterator.d.ts.map +1 -0
  31. package/dist/core/PyIterator.js +243 -0
  32. package/dist/core/PyIterator.js.map +1 -0
  33. package/dist/core/PyModule.d.ts +53 -0
  34. package/dist/core/PyModule.d.ts.map +1 -0
  35. package/dist/core/PyModule.js +162 -0
  36. package/dist/core/PyModule.js.map +1 -0
  37. package/dist/core/PyProxy.d.ts +75 -0
  38. package/dist/core/PyProxy.d.ts.map +1 -0
  39. package/dist/core/PyProxy.js +563 -0
  40. package/dist/core/PyProxy.js.map +1 -0
  41. package/dist/core/PyRuntime.d.ts +112 -0
  42. package/dist/core/PyRuntime.d.ts.map +1 -0
  43. package/dist/core/PyRuntime.js +546 -0
  44. package/dist/core/PyRuntime.js.map +1 -0
  45. package/dist/env/PackageInstaller.d.ts +232 -0
  46. package/dist/env/PackageInstaller.d.ts.map +1 -0
  47. package/dist/env/PackageInstaller.js +628 -0
  48. package/dist/env/PackageInstaller.js.map +1 -0
  49. package/dist/env/ProjectEnvironment.d.ts +63 -0
  50. package/dist/env/ProjectEnvironment.d.ts.map +1 -0
  51. package/dist/env/ProjectEnvironment.js +379 -0
  52. package/dist/env/ProjectEnvironment.js.map +1 -0
  53. package/dist/env/PyfigAdapter.d.ts +32 -0
  54. package/dist/env/PyfigAdapter.d.ts.map +1 -0
  55. package/dist/env/PyfigAdapter.js +10 -0
  56. package/dist/env/PyfigAdapter.js.map +1 -0
  57. package/dist/env/PythonDetector.d.ts +48 -0
  58. package/dist/env/PythonDetector.d.ts.map +1 -0
  59. package/dist/env/PythonDetector.js +182 -0
  60. package/dist/env/PythonDetector.js.map +1 -0
  61. package/dist/env/PythonProvisioner.d.ts +16 -0
  62. package/dist/env/PythonProvisioner.d.ts.map +1 -0
  63. package/dist/env/PythonProvisioner.js +42 -0
  64. package/dist/env/PythonProvisioner.js.map +1 -0
  65. package/dist/env/VenvManager.d.ts +55 -0
  66. package/dist/env/VenvManager.d.ts.map +1 -0
  67. package/dist/env/VenvManager.js +153 -0
  68. package/dist/env/VenvManager.js.map +1 -0
  69. package/dist/env/executables.d.ts +3 -0
  70. package/dist/env/executables.d.ts.map +1 -0
  71. package/dist/env/executables.js +14 -0
  72. package/dist/env/executables.js.map +1 -0
  73. package/dist/index.d.ts +182 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +393 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/native/NativeAddonLoader.d.ts +11 -0
  78. package/dist/native/NativeAddonLoader.d.ts.map +1 -0
  79. package/dist/native/NativeAddonLoader.js +75 -0
  80. package/dist/native/NativeAddonLoader.js.map +1 -0
  81. package/dist/plugins/Plugin.interface.d.ts +41 -0
  82. package/dist/plugins/Plugin.interface.d.ts.map +1 -0
  83. package/dist/plugins/Plugin.interface.js +12 -0
  84. package/dist/plugins/Plugin.interface.js.map +1 -0
  85. package/dist/plugins/PluginManager.d.ts +26 -0
  86. package/dist/plugins/PluginManager.d.ts.map +1 -0
  87. package/dist/plugins/PluginManager.js +174 -0
  88. package/dist/plugins/PluginManager.js.map +1 -0
  89. package/dist/plugins/builtin/NumpyPlugin.d.ts +17 -0
  90. package/dist/plugins/builtin/NumpyPlugin.d.ts.map +1 -0
  91. package/dist/plugins/builtin/NumpyPlugin.js +41 -0
  92. package/dist/plugins/builtin/NumpyPlugin.js.map +1 -0
  93. package/dist/plugins/builtin/PandasPlugin.d.ts +19 -0
  94. package/dist/plugins/builtin/PandasPlugin.d.ts.map +1 -0
  95. package/dist/plugins/builtin/PandasPlugin.js +57 -0
  96. package/dist/plugins/builtin/PandasPlugin.js.map +1 -0
  97. package/dist/plugins/builtin/TorchPlugin.d.ts +23 -0
  98. package/dist/plugins/builtin/TorchPlugin.d.ts.map +1 -0
  99. package/dist/plugins/builtin/TorchPlugin.js +50 -0
  100. package/dist/plugins/builtin/TorchPlugin.js.map +1 -0
  101. package/dist/plugins/index.d.ts +7 -0
  102. package/dist/plugins/index.d.ts.map +1 -0
  103. package/dist/plugins/index.js +12 -0
  104. package/dist/plugins/index.js.map +1 -0
  105. package/dist/serialization/DataFrameBridge.d.ts +141 -0
  106. package/dist/serialization/DataFrameBridge.d.ts.map +1 -0
  107. package/dist/serialization/DataFrameBridge.js +355 -0
  108. package/dist/serialization/DataFrameBridge.js.map +1 -0
  109. package/dist/serialization/MsgPackSerializer.d.ts +45 -0
  110. package/dist/serialization/MsgPackSerializer.d.ts.map +1 -0
  111. package/dist/serialization/MsgPackSerializer.js +242 -0
  112. package/dist/serialization/MsgPackSerializer.js.map +1 -0
  113. package/dist/serialization/NumpyBridge.d.ts +96 -0
  114. package/dist/serialization/NumpyBridge.d.ts.map +1 -0
  115. package/dist/serialization/NumpyBridge.js +323 -0
  116. package/dist/serialization/NumpyBridge.js.map +1 -0
  117. package/dist/serialization/Serializer.d.ts +78 -0
  118. package/dist/serialization/Serializer.d.ts.map +1 -0
  119. package/dist/serialization/Serializer.js +281 -0
  120. package/dist/serialization/Serializer.js.map +1 -0
  121. package/dist/types/addon.d.ts +131 -0
  122. package/dist/types/addon.d.ts.map +1 -0
  123. package/dist/types/addon.js +32 -0
  124. package/dist/types/addon.js.map +1 -0
  125. package/dist/types/config.d.ts +233 -0
  126. package/dist/types/config.d.ts.map +1 -0
  127. package/dist/types/config.js +60 -0
  128. package/dist/types/config.js.map +1 -0
  129. package/dist/types/index.d.ts +10 -0
  130. package/dist/types/index.d.ts.map +1 -0
  131. package/dist/types/index.js +12 -0
  132. package/dist/types/index.js.map +1 -0
  133. package/dist/types/python.d.ts +235 -0
  134. package/dist/types/python.d.ts.map +1 -0
  135. package/dist/types/python.js +7 -0
  136. package/dist/types/python.js.map +1 -0
  137. package/dist/utils/ErrorTranslator.d.ts +83 -0
  138. package/dist/utils/ErrorTranslator.d.ts.map +1 -0
  139. package/dist/utils/ErrorTranslator.js +210 -0
  140. package/dist/utils/ErrorTranslator.js.map +1 -0
  141. package/dist/utils/Locking.d.ts +3 -0
  142. package/dist/utils/Locking.d.ts.map +1 -0
  143. package/dist/utils/Locking.js +10 -0
  144. package/dist/utils/Locking.js.map +1 -0
  145. package/dist/utils/Logger.d.ts +27 -0
  146. package/dist/utils/Logger.d.ts.map +1 -0
  147. package/dist/utils/Logger.js +115 -0
  148. package/dist/utils/Logger.js.map +1 -0
  149. package/dist/utils/MemoryMonitor.d.ts +43 -0
  150. package/dist/utils/MemoryMonitor.d.ts.map +1 -0
  151. package/dist/utils/MemoryMonitor.js +143 -0
  152. package/dist/utils/MemoryMonitor.js.map +1 -0
  153. package/package.json +114 -4
  154. package/python/ast_inspector.py +248 -0
  155. package/python/error_handler.py +432 -0
  156. package/python/isolated_worker.py +94 -0
  157. package/python/pynect_dispatcher.py +382 -0
  158. package/python/pynect_runtime.py +574 -0
  159. package/python/serializer.py +373 -0
  160. package/python/type_inspector.py +315 -0
  161. package/scripts/build-native.js +50 -0
  162. package/scripts/cli.js +21 -0
  163. package/scripts/ensure-native.js +85 -0
  164. package/scripts/postinstall.js +40 -0
  165. package/scripts/pyfig.js +113 -0
  166. package/scripts/python-provisioner.js +23 -0
  167. package/scripts/smoke-python-boundary.js +103 -0
  168. package/src/native/NativeAddonLoader.ts +98 -0
  169. package/src/native/gil_guard.cpp +25 -0
  170. package/src/native/gil_guard.h +174 -0
  171. package/src/native/pynect_addon.cpp +942 -0
  172. package/src/native/python_bridge.cpp +904 -0
  173. package/src/native/python_bridge.h +263 -0
  174. package/src/native/thread_pool.cpp +375 -0
  175. package/src/native/thread_pool.h +185 -0
  176. package/src/native/type_converter.cpp +906 -0
  177. package/src/native/type_converter.h +271 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * pynect - PyRuntime
3
+ * The central manager for the Python interpreter lifecycle.
4
+ * Creates, configures, and shuts down the embedded CPython instance.
5
+ */
6
+ import type { NativeAddon } from '../types/addon';
7
+ import type { PynectConfig, ResolvedPynectConfig } from '../types/config';
8
+ import { PyModule } from './PyModule';
9
+ import { PyContext } from './PyContext';
10
+ import { type IsolatedCallOptions } from './IsolatedWorker';
11
+ /**
12
+ * PyRuntime - manages the lifecycle of the embedded Python interpreter.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const runtime = await PyRuntime.create({
17
+ * virtualenv: { path: '.venv', packages: ['pandas'], autoInstall: true },
18
+ * });
19
+ *
20
+ * const pd = await runtime.import('pandas');
21
+ * const df = await pd.read_csv('data.csv');
22
+ * const result = await df.to_dict('records');
23
+ *
24
+ * await runtime.shutdown();
25
+ * ```
26
+ */
27
+ export declare class PyRuntime {
28
+ private readonly _config;
29
+ private _pythonExecutable;
30
+ private readonly _pythonLibrary;
31
+ private readonly _addon;
32
+ private readonly _pluginManager;
33
+ private readonly _memoryMonitor;
34
+ private _isolatedWorker?;
35
+ private _running;
36
+ private constructor();
37
+ /**
38
+ * Create and initialize a PyRuntime instance.
39
+ */
40
+ static create(config?: PynectConfig): Promise<PyRuntime>;
41
+ private _initialize;
42
+ private _setupPythonEnvironment;
43
+ private _bootstrapPythonRuntime;
44
+ /**
45
+ * Import a Python module and return a PyProxy.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const pd = await runtime.import('pandas');
50
+ * const df = await pd.read_csv('data.csv');
51
+ * ```
52
+ */
53
+ import(moduleName: string): Promise<PyModule>;
54
+ /**
55
+ * Import multiple modules at once (concurrent).
56
+ */
57
+ importAll(moduleNames: string[]): Promise<Record<string, PyModule>>;
58
+ /**
59
+ * Call a JSON-compatible Python function in a killable child process.
60
+ * This is a process boundary, not an operating-system sandbox.
61
+ */
62
+ callIsolated<T = unknown>(moduleName: string, functionName: string, args?: unknown[], options?: IsolatedCallOptions): Promise<T>;
63
+ /**
64
+ * Evaluate a Python expression.
65
+ */
66
+ eval(expression: string): Promise<unknown>;
67
+ /**
68
+ * Execute Python statements.
69
+ */
70
+ exec(code: string): Promise<void>;
71
+ /**
72
+ * Run a Python script file.
73
+ */
74
+ runFile(filePath: string): Promise<unknown>;
75
+ /**
76
+ * Create an isolated Python execution context.
77
+ */
78
+ createContext(): Promise<PyContext>;
79
+ /**
80
+ * Install Python packages into the active environment.
81
+ */
82
+ installPackages(packages: string[]): Promise<void>;
83
+ /**
84
+ * Get current memory statistics.
85
+ */
86
+ getMemoryStats(): ReturnType<NativeAddon['getMemoryStats']>;
87
+ /**
88
+ * Trigger Python garbage collection.
89
+ */
90
+ collectGarbage(): Promise<void>;
91
+ /**
92
+ * Get the underlying native addon.
93
+ * Advanced use only.
94
+ */
95
+ getAddon(): NativeAddon;
96
+ isRunning(): boolean;
97
+ getConfig(): ResolvedPynectConfig;
98
+ /**
99
+ * Gracefully shut down the Python runtime.
100
+ * Releases all Python objects and finalizes the interpreter.
101
+ */
102
+ shutdown(): Promise<void>;
103
+ /**
104
+ * Synchronous shutdown (for process.on('exit') handler).
105
+ * Best-effort only - may not complete all cleanup.
106
+ */
107
+ shutdownSync(): void;
108
+ private _ensureRunning;
109
+ private _ensureUnsafeCodeAllowed;
110
+ private _parseBytes;
111
+ }
112
+ //# sourceMappingURL=PyRuntime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PyRuntime.d.ts","sourceRoot":"","sources":["../../src/core/PyRuntime.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAG1E,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAsB5E;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAC/C,OAAO,CAAC,eAAe,CAAC,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO;IAgBP;;OAEG;WACU,MAAM,CAAC,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;YAuKpD,WAAW;YA2DX,uBAAuB;YAMvB,uBAAuB;IAkCrC;;;;;;;;OAQG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IA2CnD;;OAEG;IACG,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAOzE;;;OAGG;IACG,YAAY,CAAC,CAAC,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,IAAI,GAAE,OAAO,EAAO,EACpB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,CAAC,CAAC;IAUb;;OAEG;IACG,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4ChD;;OAEG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvC;;OAEG;IACG,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA4BjD;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,SAAS,CAAC;IAQzC;;OAEG;IACG,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBxD;;OAEG;IACH,cAAc,IAAI,UAAU,CAAC,WAAW,CAAC,gBAAgB,CAAC,CAAC;IAK3D;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAOrC;;;OAGG;IACH,QAAQ,IAAI,WAAW;IAOvB,SAAS,IAAI,OAAO;IAIpB,SAAS,IAAI,oBAAoB;IAMjC;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB/B;;;OAGG;IACH,YAAY,IAAI,IAAI;IAYpB,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,wBAAwB;IAQhC,OAAO,CAAC,WAAW;CAUpB"}
@@ -0,0 +1,546 @@
1
+ "use strict";
2
+ /**
3
+ * pynect - PyRuntime
4
+ * The central manager for the Python interpreter lifecycle.
5
+ * Creates, configures, and shuts down the embedded CPython instance.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.PyRuntime = void 0;
12
+ const path_1 = __importDefault(require("path"));
13
+ const project_1 = require("../config/project");
14
+ const config_1 = require("../types/config");
15
+ const PyProxy_1 = require("./PyProxy");
16
+ const PyModule_1 = require("./PyModule");
17
+ const PyContext_1 = require("./PyContext");
18
+ const IsolatedWorker_1 = require("./IsolatedWorker");
19
+ const PluginManager_1 = require("../plugins/PluginManager");
20
+ const PackageInstaller_1 = require("../env/PackageInstaller");
21
+ const PyfigAdapter_1 = require("../env/PyfigAdapter");
22
+ const ProjectEnvironment_1 = require("../env/ProjectEnvironment");
23
+ const NativeAddonLoader_1 = require("../native/NativeAddonLoader");
24
+ const MemoryMonitor_1 = require("../utils/MemoryMonitor");
25
+ const Logger_1 = require("../utils/Logger");
26
+ const ErrorTranslator_1 = require("../utils/ErrorTranslator");
27
+ const logger = new Logger_1.Logger('PyRuntime');
28
+ /**
29
+ * PyRuntime - manages the lifecycle of the embedded Python interpreter.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const runtime = await PyRuntime.create({
34
+ * virtualenv: { path: '.venv', packages: ['pandas'], autoInstall: true },
35
+ * });
36
+ *
37
+ * const pd = await runtime.import('pandas');
38
+ * const df = await pd.read_csv('data.csv');
39
+ * const result = await df.to_dict('records');
40
+ *
41
+ * await runtime.shutdown();
42
+ * ```
43
+ */
44
+ class PyRuntime {
45
+ _config;
46
+ _pythonExecutable;
47
+ _pythonLibrary;
48
+ _addon;
49
+ _pluginManager;
50
+ _memoryMonitor;
51
+ _isolatedWorker;
52
+ _running = false;
53
+ constructor(config, addon, pythonLibrary) {
54
+ this._config = config;
55
+ this._pythonExecutable = config.pythonExecutable;
56
+ this._pythonLibrary = pythonLibrary;
57
+ this._addon = addon;
58
+ this._pluginManager = new PluginManager_1.PluginManager();
59
+ this._memoryMonitor = new MemoryMonitor_1.MemoryMonitor({
60
+ gcThreshold: this._parseBytes(config.gcThreshold),
61
+ });
62
+ this._memoryMonitor.attach(addon);
63
+ }
64
+ /**
65
+ * Create and initialize a PyRuntime instance.
66
+ */
67
+ static async create(config = {}) {
68
+ const project = (0, project_1.resolveProjectConfig)({
69
+ projectRoot: config.projectRoot,
70
+ python: config.python ?? config.pythonPath,
71
+ requiresPython: config.requiresPython,
72
+ managedPython: config.managedPython,
73
+ uvVersion: config.uvVersion,
74
+ installPython: config.installPython ?? config.autoInstallPython,
75
+ installUv: config.installUv ?? config.autoInstallUv,
76
+ createVirtualEnvironment: config.createVirtualEnvironment ?? config.autoCreateVirtualEnvironment,
77
+ autoSync: config.autoSync,
78
+ lockfile: config.lockfile,
79
+ env: config.env,
80
+ });
81
+ const plan = (0, ProjectEnvironment_1.createProjectPlan)({
82
+ projectRoot: project.projectRoot,
83
+ python: project.python,
84
+ requiresPython: project.requiresPython,
85
+ managedPython: project.managedPython,
86
+ uvVersion: project.uvVersion,
87
+ installPython: project.installPython,
88
+ installUv: project.installUv,
89
+ createVirtualEnvironment: project.createVirtualEnvironment,
90
+ autoSync: project.autoSync,
91
+ lockfile: project.lockfile,
92
+ env: project.env,
93
+ });
94
+ let environmentState;
95
+ if ((0, ProjectEnvironment_1.requiresProjectSync)(plan)) {
96
+ if (project.autoSync === 'if-missing') {
97
+ try {
98
+ environmentState = (0, ProjectEnvironment_1.validateProjectEnvironment)(plan);
99
+ }
100
+ catch {
101
+ environmentState = (await (0, ProjectEnvironment_1.syncProject)({
102
+ projectRoot: project.projectRoot,
103
+ python: project.python,
104
+ requiresPython: project.requiresPython,
105
+ managedPython: project.managedPython,
106
+ uvVersion: project.uvVersion,
107
+ installPython: project.installPython,
108
+ installUv: project.installUv,
109
+ createVirtualEnvironment: project.createVirtualEnvironment,
110
+ lockfile: project.lockfile,
111
+ env: project.env,
112
+ })).state;
113
+ }
114
+ }
115
+ else {
116
+ environmentState = (0, ProjectEnvironment_1.validateProjectEnvironment)(plan);
117
+ }
118
+ }
119
+ if (config.virtualenv?.autoInstall && (config.virtualenv.packages?.length ?? 0) > 0) {
120
+ throw new Error('virtualenv.autoInstall is no longer supported. Declare Python dependencies in the ' +
121
+ 'root package.json pynect.requirements field and run "pynect sync".');
122
+ }
123
+ // Resolve Python before loading the native addon. On Windows the addon
124
+ // depends on the selected Python DLL, so its directory must already be on
125
+ // PATH when Node loads the .node file.
126
+ const { python: selected } = await (0, PyfigAdapter_1.ensurePynectPython)({
127
+ autoInstallPython: project.installPython,
128
+ autoInstallUv: project.installUv,
129
+ autoCreateVirtualEnvironment: project.createVirtualEnvironment,
130
+ managedPython: project.managedPython,
131
+ uvVersion: project.uvVersion,
132
+ pythonRange: plan.pythonRange,
133
+ pythonPath: project.python,
134
+ projectRoot: project.projectRoot,
135
+ env: project.env,
136
+ });
137
+ if (environmentState) {
138
+ const selectedAbi = `${selected.implementation}-${selected.version}-` +
139
+ `${selected.architecture}-${selected.pointerBits}`;
140
+ if (!pathsEqual(environmentState.pythonExecutable, selected.executable) ||
141
+ environmentState.pythonAbi !== selectedAbi) {
142
+ throw new Error('Pynect environment Python changed since the last sync. Run "pynect sync" before ' +
143
+ 'starting the application.');
144
+ }
145
+ }
146
+ const selectedProjectRoot = selected.virtualEnvironment
147
+ ? path_1.default.dirname(selected.virtualEnvironment)
148
+ : config.projectRoot ?? process.cwd();
149
+ if (config.virtualenv?.path) {
150
+ const configuredVenv = path_1.default.resolve(config.virtualenv.path);
151
+ if (!selected.virtualEnvironment || !pathsEqual(configuredVenv, selected.virtualEnvironment)) {
152
+ throw new Error(`virtualenv.path must resolve to the repository .venv at ${selected.virtualEnvironment}.`);
153
+ }
154
+ }
155
+ const preparedConfig = {
156
+ ...config,
157
+ python: selected.executable,
158
+ requiresPython: plan.pythonRange,
159
+ projectRoot: selectedProjectRoot,
160
+ installPython: project.installPython,
161
+ installUv: project.installUv,
162
+ createVirtualEnvironment: project.createVirtualEnvironment,
163
+ managedPython: project.managedPython,
164
+ uvVersion: project.uvVersion,
165
+ autoSync: project.autoSync,
166
+ lockfile: project.lockfile,
167
+ env: project.env,
168
+ dispatcher: {
169
+ ...config.dispatcher,
170
+ include: [
171
+ ...(config.dispatcher?.include ?? []),
172
+ ...(0, ProjectEnvironment_1.moduleDispatcherIncludes)(plan),
173
+ ],
174
+ },
175
+ };
176
+ if (process.platform === 'win32') {
177
+ const requiredDirectories = [
178
+ selected.basePrefix,
179
+ selected.embedding.libraryDirectory,
180
+ path_1.default.dirname(selected.executable),
181
+ ];
182
+ const entries = (process.env['PATH'] ?? '').split(path_1.default.delimiter);
183
+ for (const directory of requiredDirectories.reverse()) {
184
+ if (!entries.some((entry) => entry.toLowerCase() === directory.toLowerCase())) {
185
+ process.env['PATH'] = `${directory}${path_1.default.delimiter}${process.env['PATH'] ?? ''}`;
186
+ }
187
+ }
188
+ }
189
+ const resolved = (0, config_1.resolveConfig)(preparedConfig);
190
+ // Set log level
191
+ Logger_1.Logger.setGlobalLevel(resolved.logLevel);
192
+ logger.info('Creating PyRuntime...');
193
+ // Load the native addon
194
+ let addon;
195
+ try {
196
+ addon = (0, NativeAddonLoader_1.loadOrBuildNativeAddon)({
197
+ projectRoot: selectedProjectRoot,
198
+ pythonExecutable: selected.executable,
199
+ });
200
+ logger.debug('Native addon loaded');
201
+ }
202
+ catch (err) {
203
+ logger.error('Failed to load the Pynect native addon.', err);
204
+ throw new Error('Pynect native addon could not be loaded or built by the active JavaScript runtime.\n' +
205
+ `Original error: ${err.message}`);
206
+ }
207
+ const runtime = new PyRuntime(resolved, addon, selected.embedding.libraryPath);
208
+ try {
209
+ await runtime._initialize();
210
+ return runtime;
211
+ }
212
+ catch (error) {
213
+ if (addon.isInitialized()) {
214
+ await addon.finalizePython().catch(() => undefined);
215
+ }
216
+ throw error;
217
+ }
218
+ }
219
+ // Initialization
220
+ async _initialize() {
221
+ const startTime = performance.now();
222
+ // 1. Detect/setup Python environment
223
+ const pythonExecutable = await this._setupPythonEnvironment();
224
+ this._pythonExecutable = pythonExecutable;
225
+ if (this._config.isolation.enabled) {
226
+ const packagedPython = path_1.default.join(__dirname, '..', '..', 'python');
227
+ this._isolatedWorker = new IsolatedWorker_1.IsolatedWorker({
228
+ pythonExecutable,
229
+ projectRoot: this._config.projectRoot,
230
+ scriptPath: path_1.default.join(packagedPython, 'isolated_worker.py'),
231
+ cwd: this._config.isolation.cwd,
232
+ timeout: this._config.isolation.timeout,
233
+ env: this._config.env,
234
+ pythonPath: [packagedPython, ...this._config.pythonPathExtra],
235
+ dispatcher: this._config.dispatcher,
236
+ includeTracebacks: this._config.includeTracebacks,
237
+ });
238
+ }
239
+ // 2. Initialize Python interpreter via C++ addon
240
+ logger.debug(`Initializing Python interpreter: ${pythonExecutable}`);
241
+ await this._addon.initializePython({
242
+ pythonHome: this._config.pythonHome || '',
243
+ pythonExecutable,
244
+ pythonLibrary: this._pythonLibrary,
245
+ threadPoolSize: this._config.threadPoolSize,
246
+ maxQueueSize: this._config.maxQueueSize,
247
+ gilReleaseInterval: this._config.gil.releaseInterval,
248
+ callTimeout: this._config.callTimeout,
249
+ pythonPathExtra: [
250
+ path_1.default.join(__dirname, '..', '..', 'python'),
251
+ ...this._config.pythonPathExtra,
252
+ ],
253
+ env: this._config.env,
254
+ enableProfiling: this._config.profilingEnabled,
255
+ includeTracebacks: this._config.includeTracebacks,
256
+ });
257
+ logger.debug('Python interpreter initialized');
258
+ // 3. Bootstrap Python runtime helpers
259
+ await this._bootstrapPythonRuntime();
260
+ // 4. Register built-in plugins
261
+ await this._pluginManager.registerBuiltins();
262
+ // 5. Start memory monitoring
263
+ this._memoryMonitor.startMonitoring(10000);
264
+ this._running = true;
265
+ const elapsed = (performance.now() - startTime).toFixed(0);
266
+ logger.success(`PyRuntime ready in ${elapsed}ms (Python: ${pythonExecutable})`);
267
+ }
268
+ async _setupPythonEnvironment() {
269
+ const pythonExecutable = this._config.pythonExecutable;
270
+ logger.info(`Using repository virtualenv: ${path_1.default.dirname(path_1.default.dirname(pythonExecutable))}`);
271
+ return pythonExecutable;
272
+ }
273
+ async _bootstrapPythonRuntime() {
274
+ try {
275
+ const dispatcherConfig = Buffer.from(JSON.stringify(this._config.dispatcher), 'utf8').toString('base64');
276
+ // Initialize pynect Python runtime helpers
277
+ const bootstrapCode = `
278
+ # Initialize optional upstream helpers. The packaged Python directory is
279
+ # already inserted into sys.path by the native initialization options.
280
+ try:
281
+ import pynect_runtime
282
+ pynect_runtime.initialize()
283
+ except ImportError:
284
+ pass # Runtime helpers not available, basic operation only
285
+
286
+ import base64 as _pynect_base64
287
+ import json as _pynect_json
288
+ import pynect_dispatcher
289
+ pynect_dispatcher.configure(
290
+ _pynect_json.loads(_pynect_base64.b64decode('${dispatcherConfig}'))
291
+ )
292
+ `;
293
+ await this._addon.runPythonCode(bootstrapCode);
294
+ logger.debug('Python runtime helpers bootstrapped');
295
+ }
296
+ catch (err) {
297
+ logger.error('Failed to bootstrap the Python dispatcher', err);
298
+ throw err;
299
+ }
300
+ }
301
+ // Module Import
302
+ /**
303
+ * Import a Python module and return a PyProxy.
304
+ *
305
+ * @example
306
+ * ```typescript
307
+ * const pd = await runtime.import('pandas');
308
+ * const df = await pd.read_csv('data.csv');
309
+ * ```
310
+ */
311
+ async import(moduleName) {
312
+ this._ensureRunning();
313
+ logger.debug(`Importing Python module: ${moduleName}`);
314
+ const result = await this._addon.importModule(moduleName);
315
+ if (!result.success) {
316
+ if (result.error) {
317
+ throw (0, ErrorTranslator_1.translatePythonError)({
318
+ type: result.error.type,
319
+ message: result.error.message,
320
+ traceback: result.error.traceback || '',
321
+ });
322
+ }
323
+ throw new Error(`Failed to import Python module '${moduleName}'`);
324
+ }
325
+ if (!result.objectId) {
326
+ throw new Error(`Module '${moduleName}' imported but no object ID returned`);
327
+ }
328
+ // Run plugin hooks
329
+ await this._pluginManager.onModuleImported(moduleName, result.objectId);
330
+ // Create PyModule
331
+ const module = PyModule_1.PyModule.fromRef(result.objectId, moduleName, this._addon, this._config.callTimeout, this._config.executionTimeout);
332
+ logger.debug(`Module '${moduleName}' imported (id=${result.objectId})`);
333
+ return module;
334
+ }
335
+ /**
336
+ * Import multiple modules at once (concurrent).
337
+ */
338
+ async importAll(moduleNames) {
339
+ const modules = await Promise.all(moduleNames.map(async (name) => [name, await this.import(name)]));
340
+ return Object.fromEntries(modules);
341
+ }
342
+ /**
343
+ * Call a JSON-compatible Python function in a killable child process.
344
+ * This is a process boundary, not an operating-system sandbox.
345
+ */
346
+ async callIsolated(moduleName, functionName, args = [], options = {}) {
347
+ this._ensureRunning();
348
+ if (!this._isolatedWorker) {
349
+ throw new Error('Isolated calls are disabled; initialize with isolation.enabled: true.');
350
+ }
351
+ return this._isolatedWorker.call(moduleName, functionName, args, options);
352
+ }
353
+ // Code Execution
354
+ /**
355
+ * Evaluate a Python expression.
356
+ */
357
+ async eval(expression) {
358
+ this._ensureRunning();
359
+ this._ensureUnsafeCodeAllowed('eval');
360
+ const result = await this._addon.evalPython(expression);
361
+ if (!result.success) {
362
+ if (result.error) {
363
+ throw (0, ErrorTranslator_1.translatePythonError)({
364
+ type: result.error.type,
365
+ message: result.error.message,
366
+ traceback: result.error.traceback || '',
367
+ });
368
+ }
369
+ throw new Error('Python eval failed');
370
+ }
371
+ if (result.isObject && result.objectId) {
372
+ return PyProxy_1.PyProxy._createFromObjectId(result.objectId, [], this._addon, this._config.callTimeout, this._config.executionTimeout);
373
+ }
374
+ if (result.resultJson !== undefined) {
375
+ try {
376
+ return JSON.parse(result.resultJson);
377
+ }
378
+ catch {
379
+ return result.resultJson;
380
+ }
381
+ }
382
+ return null;
383
+ }
384
+ /**
385
+ * Execute Python statements.
386
+ */
387
+ async exec(code) {
388
+ this._ensureRunning();
389
+ this._ensureUnsafeCodeAllowed('exec');
390
+ const result = await this._addon.runPythonCode(code);
391
+ if (!result.success && result.error) {
392
+ throw (0, ErrorTranslator_1.translatePythonError)({
393
+ type: result.error.type,
394
+ message: result.error.message,
395
+ traceback: result.error.traceback || '',
396
+ });
397
+ }
398
+ }
399
+ /**
400
+ * Run a Python script file.
401
+ */
402
+ async runFile(filePath) {
403
+ this._ensureRunning();
404
+ this._ensureUnsafeCodeAllowed('runFile');
405
+ const result = await this._addon.runPythonFile(filePath);
406
+ if (!result.success) {
407
+ if (result.error) {
408
+ throw (0, ErrorTranslator_1.translatePythonError)({
409
+ type: result.error.type,
410
+ message: result.error.message,
411
+ traceback: result.error.traceback || '',
412
+ });
413
+ }
414
+ throw new Error('Failed to run Python file');
415
+ }
416
+ return null;
417
+ }
418
+ // Context Management
419
+ /**
420
+ * Create an isolated Python execution context.
421
+ */
422
+ async createContext() {
423
+ this._ensureRunning();
424
+ this._ensureUnsafeCodeAllowed('createContext');
425
+ return PyContext_1.PyContext.create(this._addon);
426
+ }
427
+ // Package Management
428
+ /**
429
+ * Install Python packages into the active environment.
430
+ */
431
+ async installPackages(packages) {
432
+ this._ensureRunning();
433
+ if (!this._config.allowPackageInstall) {
434
+ throw new Error('Runtime package installation is disabled; set allowPackageInstall: true.');
435
+ }
436
+ const installer = new PackageInstaller_1.PackageInstaller({
437
+ pythonExecutable: this._pythonExecutable,
438
+ cwd: this._config.projectRoot,
439
+ });
440
+ await installer.install(packages);
441
+ }
442
+ // Memory Management
443
+ /**
444
+ * Get current memory statistics.
445
+ */
446
+ getMemoryStats() {
447
+ this._ensureRunning();
448
+ return this._addon.getMemoryStats();
449
+ }
450
+ /**
451
+ * Trigger Python garbage collection.
452
+ */
453
+ async collectGarbage() {
454
+ this._ensureRunning();
455
+ await this._addon.collectGarbage();
456
+ }
457
+ // Direct Addon Access
458
+ /**
459
+ * Get the underlying native addon.
460
+ * Advanced use only.
461
+ */
462
+ getAddon() {
463
+ this._ensureUnsafeCodeAllowed('getAddon');
464
+ return this._addon;
465
+ }
466
+ // State
467
+ isRunning() {
468
+ return this._running;
469
+ }
470
+ getConfig() {
471
+ return { ...this._config };
472
+ }
473
+ // Shutdown
474
+ /**
475
+ * Gracefully shut down the Python runtime.
476
+ * Releases all Python objects and finalizes the interpreter.
477
+ */
478
+ async shutdown() {
479
+ if (!this._running) {
480
+ return;
481
+ }
482
+ logger.info('Shutting down PyRuntime...');
483
+ // Stop memory monitoring
484
+ this._memoryMonitor.stopMonitoring();
485
+ await this._isolatedWorker?.close();
486
+ this._isolatedWorker = undefined;
487
+ // Finalize Python interpreter
488
+ try {
489
+ await this._addon.runPythonCode('import pynect_dispatcher as _d; _d.shutdown()');
490
+ await this._addon.finalizePython();
491
+ this._running = false;
492
+ logger.success('PyRuntime shut down');
493
+ }
494
+ catch (err) {
495
+ logger.error('Error during Python finalization', err);
496
+ throw err;
497
+ }
498
+ }
499
+ /**
500
+ * Synchronous shutdown (for process.on('exit') handler).
501
+ * Best-effort only - may not complete all cleanup.
502
+ */
503
+ shutdownSync() {
504
+ if (!this._running) {
505
+ return;
506
+ }
507
+ this._running = false;
508
+ this._memoryMonitor.stopMonitoring();
509
+ this._isolatedWorker?.closeSync();
510
+ this._isolatedWorker = undefined;
511
+ // Cannot await here - best effort
512
+ logger.debug('Synchronous shutdown initiated');
513
+ }
514
+ // Private Helpers
515
+ _ensureRunning() {
516
+ if (!this._running) {
517
+ throw new ErrorTranslator_1.PynectShutdownError('PyRuntime has been shut down. Create a new runtime with PyRuntime.create().');
518
+ }
519
+ }
520
+ _ensureUnsafeCodeAllowed(operation) {
521
+ if (!this._config.allowUnsafeCode) {
522
+ throw new Error(`${operation} is disabled by default; set allowUnsafeCode: true for trusted local code.`);
523
+ }
524
+ }
525
+ _parseBytes(str) {
526
+ const match = str.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)?$/i);
527
+ if (!match) {
528
+ return 1_073_741_824;
529
+ }
530
+ const value = parseFloat(match[1]);
531
+ const unit = (match[2] ?? 'B').toUpperCase();
532
+ const units = {
533
+ B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4,
534
+ };
535
+ return Math.floor(value * (units[unit] ?? 1));
536
+ }
537
+ }
538
+ exports.PyRuntime = PyRuntime;
539
+ function pathsEqual(left, right) {
540
+ const normalize = (value) => {
541
+ const resolved = path_1.default.resolve(value);
542
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
543
+ };
544
+ return normalize(left) === normalize(right);
545
+ }
546
+ //# sourceMappingURL=PyRuntime.js.map