stata-code 0.3.0__py3-none-any.whl
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.
- stata_code/__init__.py +100 -0
- stata_code/core/__init__.py +73 -0
- stata_code/core/_pool.py +808 -0
- stata_code/core/_refs.py +97 -0
- stata_code/core/_runtime.py +179 -0
- stata_code/core/errors.py +447 -0
- stata_code/core/runner.py +1092 -0
- stata_code/core/schema.py +317 -0
- stata_code/kernel/__init__.py +5 -0
- stata_code/kernel/__main__.py +6 -0
- stata_code/kernel/kernel.py +331 -0
- stata_code/mcp/__init__.py +3 -0
- stata_code/mcp/__main__.py +6 -0
- stata_code/mcp/server.py +360 -0
- stata_code-0.3.0.dist-info/METADATA +389 -0
- stata_code-0.3.0.dist-info/RECORD +20 -0
- stata_code-0.3.0.dist-info/WHEEL +4 -0
- stata_code-0.3.0.dist-info/entry_points.txt +3 -0
- stata_code-0.3.0.dist-info/licenses/LICENSE +21 -0
- stata_code-0.3.0.dist-info/licenses/LICENSE-POLICY.md +125 -0
stata_code/core/_refs.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Process-wide ref store for log/graph/matrix payloads.
|
|
2
|
+
|
|
3
|
+
`stata_code` returns large payloads (full logs, graph bytes, matrix values)
|
|
4
|
+
by reference rather than inline, to keep agent token economy in check. This
|
|
5
|
+
module owns the underlying dict; auxiliary tools (`get_log`, `get_graph`,
|
|
6
|
+
`get_matrix`) read from it.
|
|
7
|
+
|
|
8
|
+
Refs are valid only within the lifetime of the producing process. Per
|
|
9
|
+
SCHEMA.md §3.3: consumers MUST NOT persist refs across sessions.
|
|
10
|
+
|
|
11
|
+
Eviction: the store is bounded by `MAX_ENTRIES` (default 256) on an LRU
|
|
12
|
+
basis. When inserting beyond the cap, the least-recently-used entry is
|
|
13
|
+
dropped. This guards long-running MCP server processes from unbounded
|
|
14
|
+
growth. Adjust via `set_capacity(n)` if you need more slack.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import threading
|
|
20
|
+
from collections import OrderedDict
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
DEFAULT_MAX_ENTRIES = 256
|
|
24
|
+
|
|
25
|
+
_lock = threading.Lock()
|
|
26
|
+
_store: OrderedDict[str, Any] = OrderedDict()
|
|
27
|
+
_max_entries: int = DEFAULT_MAX_ENTRIES
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_capacity(n: int) -> None:
|
|
31
|
+
"""Set the maximum number of refs to retain. Must be ≥ 1."""
|
|
32
|
+
global _max_entries
|
|
33
|
+
if n < 1:
|
|
34
|
+
raise ValueError("capacity must be ≥ 1")
|
|
35
|
+
with _lock:
|
|
36
|
+
_max_entries = n
|
|
37
|
+
_evict_to_capacity_locked()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_capacity() -> int:
|
|
41
|
+
with _lock:
|
|
42
|
+
return _max_entries
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def put(ref: str, payload: Any) -> None:
|
|
46
|
+
with _lock:
|
|
47
|
+
# If already present, move-to-end keeps the entry "fresh".
|
|
48
|
+
if ref in _store:
|
|
49
|
+
_store.move_to_end(ref)
|
|
50
|
+
_store[ref] = payload
|
|
51
|
+
_evict_to_capacity_locked()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get(ref: str) -> Any | None:
|
|
55
|
+
"""Fetch a payload. Touches LRU order so frequently-used refs survive."""
|
|
56
|
+
with _lock:
|
|
57
|
+
if ref not in _store:
|
|
58
|
+
return None
|
|
59
|
+
_store.move_to_end(ref)
|
|
60
|
+
return _store[ref]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def has(ref: str) -> bool:
|
|
64
|
+
with _lock:
|
|
65
|
+
return ref in _store
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def discard(ref: str) -> None:
|
|
69
|
+
with _lock:
|
|
70
|
+
_store.pop(ref, None)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clear_prefix(prefix: str) -> int:
|
|
74
|
+
"""Drop all refs whose key starts with `prefix`. Returns count dropped."""
|
|
75
|
+
with _lock:
|
|
76
|
+
keys = [k for k in _store if k.startswith(prefix)]
|
|
77
|
+
for k in keys:
|
|
78
|
+
del _store[k]
|
|
79
|
+
return len(keys)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def clear_all() -> None:
|
|
83
|
+
"""Drop every ref. Mainly for tests."""
|
|
84
|
+
with _lock:
|
|
85
|
+
_store.clear()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def size() -> int:
|
|
89
|
+
with _lock:
|
|
90
|
+
return len(_store)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _evict_to_capacity_locked() -> None:
|
|
94
|
+
"""Drop oldest entries until len(_store) <= _max_entries. Caller holds _lock."""
|
|
95
|
+
while len(_store) > _max_entries:
|
|
96
|
+
# popitem(last=False) drops the OLDEST (FIFO end of the OrderedDict)
|
|
97
|
+
_store.popitem(last=False)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Private pystata runtime wrapper — process-wide singleton.
|
|
2
|
+
|
|
3
|
+
pystata initializes one Stata kernel per Python process. This module manages
|
|
4
|
+
that lifecycle and exposes a small surface (`run_capture`, sfi accessors) for
|
|
5
|
+
the public runner. Not part of the API; subject to change without notice.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
import threading
|
|
14
|
+
from contextlib import redirect_stdout
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# Common pystata install locations across platforms. Order is best-guess for
|
|
19
|
+
# popularity; the first match wins. Users can also pre-install pystata into
|
|
20
|
+
# their Python environment, in which case we skip the path search entirely.
|
|
21
|
+
_PYSTATA_SEARCH_PATHS: tuple[str, ...] = (
|
|
22
|
+
"/Applications/Stata/utilities",
|
|
23
|
+
"/Applications/Stata18/utilities",
|
|
24
|
+
"/Applications/Stata17/utilities",
|
|
25
|
+
"/usr/local/stata18/utilities",
|
|
26
|
+
"/usr/local/stata17/utilities",
|
|
27
|
+
r"C:\Program Files\Stata18\utilities",
|
|
28
|
+
r"C:\Program Files\Stata17\utilities",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_RC_RE = re.compile(r"r\((-?\d+)\);")
|
|
32
|
+
|
|
33
|
+
_lock = threading.Lock()
|
|
34
|
+
_runtime: PystataRuntime | None = None
|
|
35
|
+
_init_attempted = False
|
|
36
|
+
_init_error: Exception | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PystataNotAvailable(RuntimeError):
|
|
40
|
+
"""Raised when pystata cannot be imported or Stata cannot be initialized."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PystataRuntime:
|
|
44
|
+
"""Wraps a single pystata-initialized Stata kernel.
|
|
45
|
+
|
|
46
|
+
Construct via `get_runtime()`; do not instantiate directly. The kernel
|
|
47
|
+
persists for the lifetime of the Python process.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self._initialized = False
|
|
52
|
+
self._stata: Any = None
|
|
53
|
+
self._config: Any = None
|
|
54
|
+
self._sfi: Any = None
|
|
55
|
+
self._edition: str = ""
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _find_pystata_path() -> str | None:
|
|
59
|
+
try:
|
|
60
|
+
import pystata # noqa: F401
|
|
61
|
+
|
|
62
|
+
return None # already importable; nothing to add to sys.path
|
|
63
|
+
except ImportError:
|
|
64
|
+
pass
|
|
65
|
+
for p in _PYSTATA_SEARCH_PATHS:
|
|
66
|
+
if Path(p).joinpath("pystata").is_dir():
|
|
67
|
+
return p
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _initialize(self) -> None:
|
|
71
|
+
if self._initialized:
|
|
72
|
+
return
|
|
73
|
+
path = self._find_pystata_path()
|
|
74
|
+
if path is not None and path not in sys.path:
|
|
75
|
+
sys.path.insert(0, path)
|
|
76
|
+
try:
|
|
77
|
+
from pystata import config as cfg
|
|
78
|
+
except ImportError as exc:
|
|
79
|
+
raise PystataNotAvailable(
|
|
80
|
+
"pystata is not importable; install Stata 17+ or set PYTHONPATH"
|
|
81
|
+
) from exc
|
|
82
|
+
|
|
83
|
+
last_err: Exception | None = None
|
|
84
|
+
for ed in ("mp", "se", "be"):
|
|
85
|
+
try:
|
|
86
|
+
cfg.init(ed, splash=False)
|
|
87
|
+
self._edition = ed
|
|
88
|
+
break
|
|
89
|
+
except Exception as exc: # noqa: BLE001
|
|
90
|
+
last_err = exc
|
|
91
|
+
else:
|
|
92
|
+
raise PystataNotAvailable(
|
|
93
|
+
f"pystata.config.init failed for all editions: {last_err}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
cfg.set_graph_show(False)
|
|
97
|
+
cfg.set_graph_format("png")
|
|
98
|
+
|
|
99
|
+
import sfi # type: ignore[import-not-found]
|
|
100
|
+
from pystata import stata as st # must be imported after config.init
|
|
101
|
+
|
|
102
|
+
self._config = cfg
|
|
103
|
+
self._stata = st
|
|
104
|
+
self._sfi = sfi
|
|
105
|
+
self._initialized = True
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def is_initialized(self) -> bool:
|
|
109
|
+
return self._initialized
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def edition(self) -> str:
|
|
113
|
+
return self._edition
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def stata(self) -> Any:
|
|
117
|
+
return self._stata
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def sfi(self) -> Any:
|
|
121
|
+
return self._sfi
|
|
122
|
+
|
|
123
|
+
def run_capture(self, code: str) -> tuple[str, int, str | None]:
|
|
124
|
+
"""Run Stata code and return (captured_stdout, rc, error_message).
|
|
125
|
+
|
|
126
|
+
- rc == 0 on success.
|
|
127
|
+
- rc > 0 is Stata's `_rc` (parsed from the SystemError message).
|
|
128
|
+
- rc == -1 is an adapter-level crash (non-Stata exception).
|
|
129
|
+
- error_message is None on success, non-None on failure.
|
|
130
|
+
"""
|
|
131
|
+
buf = io.StringIO()
|
|
132
|
+
try:
|
|
133
|
+
with redirect_stdout(buf):
|
|
134
|
+
self._stata.run(code, quietly=False, echo=False)
|
|
135
|
+
return buf.getvalue(), 0, None
|
|
136
|
+
except SystemError as exc:
|
|
137
|
+
err_text = str(exc.args[0]) if exc.args else str(exc)
|
|
138
|
+
m = _RC_RE.search(err_text)
|
|
139
|
+
rc = int(m.group(1)) if m else -1
|
|
140
|
+
return buf.getvalue(), rc, err_text.strip()
|
|
141
|
+
except Exception as exc: # noqa: BLE001
|
|
142
|
+
return buf.getvalue(), -1, f"{type(exc).__name__}: {exc}"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_runtime() -> PystataRuntime:
|
|
146
|
+
"""Return the process-wide runtime, initializing on first call.
|
|
147
|
+
|
|
148
|
+
Raises `PystataNotAvailable` if pystata or Stata cannot be initialized.
|
|
149
|
+
The failure is cached: subsequent calls re-raise the same error without
|
|
150
|
+
retrying init (which would print messy errors repeatedly).
|
|
151
|
+
"""
|
|
152
|
+
global _runtime, _init_attempted, _init_error
|
|
153
|
+
if _runtime is not None:
|
|
154
|
+
return _runtime
|
|
155
|
+
if _init_attempted and _init_error is not None:
|
|
156
|
+
raise _init_error # cached failure
|
|
157
|
+
with _lock:
|
|
158
|
+
if _runtime is not None:
|
|
159
|
+
return _runtime
|
|
160
|
+
_init_attempted = True
|
|
161
|
+
try:
|
|
162
|
+
rt = PystataRuntime()
|
|
163
|
+
rt._initialize()
|
|
164
|
+
_runtime = rt
|
|
165
|
+
return _runtime
|
|
166
|
+
except Exception as exc: # noqa: BLE001
|
|
167
|
+
_init_error = exc if isinstance(exc, PystataNotAvailable) else (
|
|
168
|
+
PystataNotAvailable(f"runtime init failed: {exc}")
|
|
169
|
+
)
|
|
170
|
+
raise _init_error from exc
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def is_available() -> bool:
|
|
174
|
+
"""Cheap, non-raising check. Initializes on first call."""
|
|
175
|
+
try:
|
|
176
|
+
get_runtime()
|
|
177
|
+
return True
|
|
178
|
+
except PystataNotAvailable:
|
|
179
|
+
return False
|