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