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 +24 -0
- package/LICENSE-OpenDTA +26 -0
- package/README.md +109 -2
- package/index.d.ts +119 -0
- package/index.mjs +136 -0
- package/opendta.json +1 -0
- package/opendta_bridge.py +472 -0
- package/package.json +51 -4
- package/scripts/bundle-opendta.py +56 -0
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.
|
package/LICENSE-OpenDTA
ADDED
|
@@ -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
|
-
|
|
1
|
+
<h1 align="center">opendta-pyodide</h1>
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|