opendta-pyodide 0.0.0-stage → 0.2.3

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.
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NakliTechie
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.
22
+
23
+ opendta.json contains the modules of OpenDTA, Copyright (c) 2026 OpenDTA
24
+ contributors, under the MIT licence in LICENSE-OpenDTA.
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenDTA contributors
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.
22
+
23
+ Trademark Notice
24
+
25
+ Stata is a registered trademark of StataCorp LLC. OpenDTA is an independent
26
+ project and is not affiliated with, endorsed by, or sponsored by StataCorp LLC.
package/README.md CHANGED
@@ -1,3 +1,110 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">opendta-pyodide</h1>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <p align="center"><b>Run Stata 14 commands, graphs and Mata in the browser or Node.<br>
4
+ OpenDTA's open-source engine, packaged for Pyodide, with a bridge to your own data.</b></p>
5
+
6
+ <p align="center">Pyodide 0.27, NumPy and SciPy (matplotlib for graphs). No server. MIT.</p>
7
+
8
+ <p align="center">
9
+ <img alt="npm" src="https://img.shields.io/npm/v/opendta-pyodide?style=flat-square&color=555">
10
+ <img alt="licence MIT" src="https://img.shields.io/badge/licence-MIT-555?style=flat-square">
11
+ <img alt="runtime Pyodide 0.27" src="https://img.shields.io/badge/runtime-Pyodide%200.27-555?style=flat-square">
12
+ </p>
13
+
14
+ ## Install
15
+
16
+ | Where | How |
17
+ |---|---|
18
+ | npm | `npm install opendta-pyodide pyodide@0.27.7` |
19
+ | CDN | `import { createSession } from 'https://cdn.jsdelivr.net/npm/opendta-pyodide@0.2.3/index.mjs'` |
20
+ | GitHub tag | `https://cdn.jsdelivr.net/gh/NakliTechie/opendta-pyodide@v0.2.3/index.mjs` |
21
+
22
+ ```js
23
+ import { loadPyodide } from 'pyodide';
24
+ import { createSession } from 'opendta-pyodide';
25
+
26
+ const stata = await createSession(await loadPyodide()); // loads numpy and scipy once
27
+ await stata.stata('set obs 100\ngen x = runiform()\ngen y = 2*x + rnormal()');
28
+ const out = await stata.stata('regress y x\nmargins, dydx(x)');
29
+ console.log(out.log); // the log as Stata 14 prints it
30
+ const g = await stata.stata('scatter y x');
31
+ document.body.innerHTML = g.graphs[0]; // each graph as SVG text
32
+ const m = await stata.mata('A = (4,2\\2,3)\ncholesky(A)'); // Mata, in the same package
33
+ ```
34
+
35
+ `createSession` fetches `opendta.json` (4.9 MB, 0.9 MB with brotli) and
36
+ `opendta_bridge.py` from beside `index.mjs`. A command that draws loads matplotlib first.
37
+
38
+ ## Why
39
+
40
+ You have Stata do-files and want them to run where your users are, in a web page or a
41
+ Node script, without a Stata licence. [OpenDTA](https://github.com/yaskepalhares/opendta)
42
+ is an MIT Python reimplementation of Stata 14: data management, estimation and
43
+ post-estimation, programs and `syntax`, graphs drawn with matplotlib, and a Mata
44
+ interpreter. This package ships its engine (287 modules, without the desktop interface),
45
+ and a bridge so the engine reads and writes a dataset that your own code owns.
46
+
47
+ How close OpenDTA is to Stata is OpenDTA's claim, checked by its 108 Stata 14 reference
48
+ logs. NakliData, a browser data workbench, runs 9 of them (the Mata files) through this
49
+ bridge: 7 print logs equal to Stata 14's, and the other 2 differ where OpenDTA lists known
50
+ differences.
51
+
52
+ ## Give the engine your data
53
+
54
+ `runRequest` takes the dataset as columns, plus macros and scalars, runs Stata command
55
+ lines, and returns what changed:
56
+
57
+ ```js
58
+ import { runRequest } from 'opendta-pyodide';
59
+
60
+ const out = await runRequest(pyodide, {
61
+ kind: 'command',
62
+ nobs: 4,
63
+ vars: [['x', 'num', 'DOUBLE']],
64
+ columns: [new Float64Array([4, 1, 3, NaN])], // NaN is Stata's missing value
65
+ code: 'xtile q = x, nq(2)',
66
+ });
67
+ out.data.columns; // only the new or changed variables: [{ name: 'q', values: [2, 1, 2, null] }]
68
+ ```
69
+
70
+ The response holds `log`, `error` (`{ code, message }` with Stata's return code), the
71
+ changed `data`, `locals`, `globals`, `scalars`, `r`, `e`, `ematrix` (e(b) and e(V)),
72
+ `graphs` and the random-number state. Pass `rng` back so `runiform()` continues Stata 14's
73
+ stream. Use `kind: 'block'` for Mata and `kind: 'matrix'` for a `matrix` command (without
74
+ the word `matrix`).
75
+
76
+ ## Call back into your host
77
+
78
+ From Mata, `stata("...")` calls your `stata: async (stateJson) => reply`, and expressions
79
+ in `matrix` commands call `evaluate`. Both wait on your async code through Pyodide's
80
+ `run_sync`, which needs WebAssembly stack switching (JSPI): on by default in current
81
+ Chrome, and behind `--experimental-wasm-jspi` in Node 22. Without JSPI the call stops with
82
+ Stata error 3000 and says why. The formats are in `index.d.ts`.
83
+
84
+ ## Commands
85
+
86
+ ```sh
87
+ npm test # 15 tests in Node: commands, data, e(b), graphs, Mata
88
+ python3 -I scripts/bundle-opendta.py <opendta checkout> # rebuild opendta.json at the pin
89
+ ```
90
+
91
+ ## Verify it yourself
92
+
93
+ ```sh
94
+ npm ci && npm test
95
+ ```
96
+
97
+ The tests run in Pyodide 0.27.7. They check `tabstat` output, a host dataset round trip
98
+ through `xtile` that returns only the new variable, `poisson` results in `e()` and e(b),
99
+ error 199 for an unknown command, a `scatter` drawn as SVG, and the Mata path: printed
100
+ matrices, a singular matrix, Stata 14's first `runiform()` draw for seed 123, a `stata()`
101
+ callback and a `matrix` command. The bundle pins OpenDTA commit `29a3b28`. Pyodide's
102
+ NumPy returns NaN from `cholesky`, `inv` and `solve` instead of raising, because
103
+ WebAssembly has no floating-point flags; the bridge restores the `LinAlgError` that
104
+ OpenDTA expects.
105
+
106
+ ## Licence
107
+
108
+ MIT. OpenDTA is Copyright (c) 2026 OpenDTA contributors, MIT (`LICENSE-OpenDTA`).
109
+
110
+ [OpenDTA](https://github.com/yaskepalhares/opendta) · [bridge](opendta_bridge.py) · [types](index.d.ts)
package/index.d.ts ADDED
@@ -0,0 +1,119 @@
1
+ // Types for opendta-pyodide.
2
+
3
+ /** The slice of the Pyodide API this package uses (Pyodide 0.27). */
4
+ export interface PyodideLike {
5
+ runPython(code: string): unknown;
6
+ runPythonAsync(code: string): Promise<unknown>;
7
+ loadPackage(names: string[]): Promise<unknown>;
8
+ globals: { set(name: string, value: unknown): void; delete(name: string): void };
9
+ FS: { mkdirTree(path: string): void; writeFile(path: string, data: string): void };
10
+ }
11
+
12
+ type Value = number | string | null;
13
+
14
+
15
+ /** What the host holds, as Mata reads it. Every field is optional in a request. */
16
+ export interface HostState {
17
+ nobs: number;
18
+ /** [name, 'num' | 'str' | 'date', SQL type] per variable, in dataset order. The SQL type sets
19
+ * Stata's storage type: TINYINT byte, SMALLINT int, INTEGER long, FLOAT float, else double. */
20
+ vars: Array<[string, 'num' | 'str' | 'date', string]>;
21
+ /** Float64Array (NaN = missing; dates as days since 1960-01-01) or strings. */
22
+ columns: Array<Float64Array | string[]>;
23
+ locals: Record<string, string>;
24
+ globals: Record<string, string>;
25
+ scalars: Record<string, Value>;
26
+ r: Record<string, Value>;
27
+ e: Record<string, Value>;
28
+ /** Stata 14's random-number state ("X" + hex), as returned in a response's `rng`. Empty for a fresh seed. */
29
+ rng: string;
30
+ /** e(b) and e(V) of the last estimation, for `matrix list e(b)` and st_matrix("e(b)"). */
31
+ ematrix: { names: string[]; b: number[]; V: number[][] } | null;
32
+ }
33
+
34
+ /**
35
+ * Reply to a `stata()` call from Mata: the host's full state after the command
36
+ * (every HostState field except columns, plus `log: string[]` and
37
+ * `error: {code, message} | null`) as JSON, and the columns separately.
38
+ */
39
+ export interface StataReply {
40
+ json: string;
41
+ columns: Array<Float64Array | string[]>;
42
+ }
43
+
44
+ export interface OpenDTARequest extends Partial<HostState> {
45
+ /** Stata command lines, a `mata` block, a `mata:` line, or a Stata `matrix` command. */
46
+ kind?: 'command' | 'block' | 'line' | 'matrix';
47
+ /** For `command`: echo each line as `. cmd` in the log, as a do-file does. Default false. */
48
+ echo?: boolean;
49
+ /** For `command`: ignore the host fields and keep OpenDTA's own dataset and results. */
50
+ keepData?: boolean;
51
+ /** Block body (the lines between `mata` and `end`), one statement, or a matrix command without the word `matrix`. */
52
+ code: string;
53
+ /** `mata:` stops at the first error; `mata` reports it and carries on. Default false. */
54
+ colon?: boolean;
55
+ /** Start a fresh Mata session (functions and globals cleared). Default false. */
56
+ reset?: boolean;
57
+ /** `set linesize`: Mata wraps matrices and rules at this width. Default 80. */
58
+ linesize?: number;
59
+ /** Runs `stata("cmd")`: receives Mata's changes as JSON (StateChanges plus `cmd`). Needs JSPI. */
60
+ stata?: (stateJson: string) => Promise<StataReply>;
61
+ /** Evaluates a Stata scalar expression for `matrix`; returns JSON `{"value": number | string | null}` or `{"error": {code, message}}`. Needs JSPI. */
62
+ evaluate?: (expr: string) => Promise<string>;
63
+ }
64
+
65
+ export interface HostColumn {
66
+ name: string;
67
+ kind: 'num' | 'str' | 'date';
68
+ integer?: boolean;
69
+ values: Value[];
70
+ }
71
+
72
+ /** What Mata changed. `data` is null when the dataset is unchanged. */
73
+ export interface StateChanges {
74
+ data: { full: boolean; nobs: number; order: string[]; columns: HostColumn[] } | null;
75
+ locals: Record<string, string>;
76
+ globals: Record<string, string>;
77
+ scalars: Record<string, Value>;
78
+ /** null when Mata left r() / e() unchanged. */
79
+ r: Record<string, Value> | null;
80
+ e: Record<string, Value> | null;
81
+ rng: string;
82
+ }
83
+
84
+ export interface OpenDTAResponse extends StateChanges {
85
+ log: string;
86
+ error: { code: number; message: string } | null;
87
+ /** For `command`: e(b) and e(V) after an estimation, else null. */
88
+ ematrix?: { names: string[]; b: number[]; V: number[][] } | null;
89
+ /** For `command`: each graph drawn, as SVG text. */
90
+ graphs?: string[];
91
+ }
92
+
93
+ /** The parsed opendta.json. */
94
+ export interface OpenDTABundle {
95
+ source: string;
96
+ commit: string;
97
+ licence: string;
98
+ patched: string[];
99
+ files: Record<string, string>;
100
+ }
101
+
102
+ export interface InstallOptions {
103
+ /** URL of opendta.json (relative to this module), or the parsed bundle. */
104
+ bundle?: string | URL | OpenDTABundle;
105
+ /** URL of opendta_bridge.py (relative to this module). */
106
+ bridge?: string | URL;
107
+ }
108
+
109
+ export function installOpenDTA(pyodide: PyodideLike, options?: InstallOptions): Promise<{ source: string; commit: string }>;
110
+ export function runRequest(pyodide: PyodideLike, request: OpenDTARequest): Promise<OpenDTAResponse>;
111
+ export function drawsGraph(code: string): boolean;
112
+ export function createSession(
113
+ pyodide: PyodideLike,
114
+ options?: InstallOptions,
115
+ ): Promise<{
116
+ stata(code: string, request?: OpenDTARequest): Promise<OpenDTAResponse>;
117
+ mata(code: string, request?: OpenDTARequest): Promise<OpenDTAResponse>;
118
+ reset(): void;
119
+ }>;
package/index.mjs ADDED
@@ -0,0 +1,136 @@
1
+ // opendta-pyodide: run Stata 14 commands and Mata in the browser or Node, on
2
+ // Pyodide. The engine is OpenDTA (https://github.com/yaskepalhares/opendta,
3
+ // MIT), bundled in opendta.json; opendta_bridge.py connects it to a host that
4
+ // owns the dataset, macros and scalars.
5
+
6
+ const ROOT = '/opendta_pyodide';
7
+ const installed = new WeakMap();
8
+ const withGraphs = new WeakSet();
9
+
10
+ // Commands that draw: their first word, as Stata accepts it (abbreviations included).
11
+ const GRAPH = /^\s*(?:(?:quietly|qui|noisily|noi|capture|cap)\s+)*(?:gr(?:a(?:ph?)?)?|tw(?:o(?:w(?:ay?)?)?)?|sc(?:a(?:t(?:t(?:er?)?)?)?)?|line|connected|area|bar|hist(?:o(?:g(?:r(?:am?)?)?)?)?|kdensity|kdens|qnorm|pnorm|qqplot|lowess|binscatter|marginsplot|coefplot|rvfplot|avplots?|lvr2plot|graph\s+\w+)\b/m;
12
+
13
+ async function text(source, fallback) {
14
+ const u = new URL(source ?? fallback, import.meta.url);
15
+ if (u.protocol === 'file:') {
16
+ const { readFile } = await import('node:fs/promises');
17
+ return readFile(u, 'utf8');
18
+ }
19
+ const res = await fetch(u);
20
+ if (!res.ok) throw new Error(`opendta-pyodide: could not fetch ${u.href} (HTTP ${res.status})`);
21
+ return res.text();
22
+ }
23
+
24
+ /**
25
+ * Install OpenDTA into a Pyodide instance: loads numpy and scipy, writes the
26
+ * OpenDTA modules and the bridge into Pyodide's file system, and imports the
27
+ * bridge. `bundle` and `bridge` are URLs (relative to this module) of
28
+ * opendta.json and opendta_bridge.py, defaulting to the copies next to this
29
+ * module; `bundle` may also be the parsed bundle object.
30
+ */
31
+ export async function installOpenDTA(pyodide, { bundle, bridge } = {}) {
32
+ if (installed.has(pyodide)) return installed.get(pyodide);
33
+ const job = (async () => {
34
+ const b = bundle?.files ? bundle : JSON.parse(await text(bundle, './opendta.json'));
35
+ const py = await text(bridge, './opendta_bridge.py');
36
+ await pyodide.loadPackage(['numpy', 'scipy']);
37
+ for (const [path, body] of Object.entries(b.files)) {
38
+ pyodide.FS.mkdirTree(`${ROOT}/${path}`.replace(/\/[^/]+$/, ''));
39
+ pyodide.FS.writeFile(`${ROOT}/${path}`, body);
40
+ }
41
+ pyodide.FS.mkdirTree(ROOT);
42
+ pyodide.FS.writeFile(`${ROOT}/opendta_bridge.py`, py);
43
+ pyodide.runPython(
44
+ `import sys\nif ${JSON.stringify(ROOT)} not in sys.path: sys.path.insert(0, ${JSON.stringify(ROOT)})\nimport opendta_bridge`,
45
+ );
46
+ return { source: b.source, commit: b.commit };
47
+ })();
48
+ installed.set(pyodide, job);
49
+ job.catch(() => installed.delete(pyodide));
50
+ return job;
51
+ }
52
+
53
+ const DEFAULTS = {
54
+ kind: 'block',
55
+ colon: false,
56
+ reset: false,
57
+ echo: false,
58
+ linesize: 80,
59
+ nobs: 0,
60
+ vars: [],
61
+ columns: [],
62
+ locals: {},
63
+ globals: {},
64
+ scalars: {},
65
+ r: {},
66
+ e: {},
67
+ rng: '',
68
+ ematrix: null,
69
+ };
70
+
71
+ /** True when Stata command text draws a graph (so matplotlib must be loaded first). */
72
+ export function drawsGraph(code) {
73
+ return GRAPH.test(code);
74
+ }
75
+
76
+ /**
77
+ * Run one request: Stata command lines (`kind: 'command'`), a Mata block, one
78
+ * `mata:` line, or a Stata `matrix` command. Returns the log, any error, what
79
+ * the code changed, and (for commands) the graphs drawn, as SVG text.
80
+ */
81
+ export async function runRequest(pyodide, request) {
82
+ await installOpenDTA(pyodide);
83
+ const { columns, stata, evaluate, ...meta } = { ...DEFAULTS, ...request };
84
+ if (meta.kind === 'command' && !withGraphs.has(pyodide) && drawsGraph(meta.code)) {
85
+ await pyodide.loadPackage(['matplotlib']);
86
+ withGraphs.add(pyodide);
87
+ }
88
+ const g = pyodide.globals;
89
+ g.set('__od_meta', JSON.stringify(meta));
90
+ g.set('__od_cols', columns);
91
+ g.set('__od_host', stata ?? null);
92
+ g.set('__od_eval', evaluate ?? null);
93
+ try {
94
+ // runPythonAsync lets stata() and matrix expressions wait on the host through
95
+ // Pyodide's run_sync, which needs WebAssembly stack switching (JSPI).
96
+ const out = await pyodide.runPythonAsync(
97
+ 'opendta_bridge.run_json(__od_meta, __od_cols, __od_host, __od_eval)',
98
+ );
99
+ return JSON.parse(out);
100
+ } finally {
101
+ for (const k of ['__od_meta', '__od_cols', '__od_host', '__od_eval']) g.delete(k);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * A session with no host dataset: `stata(lines)` runs Stata commands on
107
+ * OpenDTA's own dataset, which persists between calls; `mata(code)` runs a
108
+ * Mata block. Matrices are shared between the two. `reset()` starts both over.
109
+ */
110
+ export async function createSession(pyodide, options) {
111
+ await installOpenDTA(pyodide, options);
112
+ // One reset for both: commands and Mata share matrices, so neither may clear the other.
113
+ let fresh = true;
114
+ return {
115
+ async stata(code, request = {}) {
116
+ const out = await runRequest(pyodide, {
117
+ echo: true,
118
+ ...request,
119
+ kind: 'command',
120
+ code,
121
+ reset: fresh,
122
+ keepData: true,
123
+ });
124
+ fresh = false;
125
+ return out;
126
+ },
127
+ async mata(code, request = {}) {
128
+ const out = await runRequest(pyodide, { ...request, kind: 'block', code, reset: fresh });
129
+ fresh = false;
130
+ return out;
131
+ },
132
+ reset() {
133
+ fresh = true;
134
+ },
135
+ };
136
+ }