stata-cli 0.4.1__tar.gz → 0.5.0__tar.gz
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_cli-0.4.1 → stata_cli-0.5.0}/PKG-INFO +1 -1
- {stata_cli-0.4.1 → stata_cli-0.5.0}/pyproject.toml +1 -1
- stata_cli-0.5.0/src/stata_cli/__init__.py +1 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/daemon.py +94 -29
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/main.py +60 -37
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli.egg-info/PKG-INFO +1 -1
- stata_cli-0.4.1/src/stata_cli/__init__.py +0 -1
- {stata_cli-0.4.1 → stata_cli-0.5.0}/README.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/setup.cfg +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/__main__.py +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/engine.py +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/graph_artifacts.py +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/output_filter.py +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skill_registry.py +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/overview.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/asdoc.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/binsreg.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/coefplot.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/data-manipulation.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/diagnostics.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/did.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/estout.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/event-study.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/graph-schemes.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/ivreg2.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/nprobust.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/outreg2.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/package-management.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/psmatch2.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/rdrobust.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/reghdfe.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/synth.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/tabout.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/winsor.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/packages/xtabond2.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/advanced-programming.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/basics-getting-started.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/bootstrap-simulation.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/data-import-export.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/data-management.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/date-time-functions.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/descriptive-statistics.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/difference-in-differences.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/external-tools-integration.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/gmm-estimation.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/graphics.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/limited-dependent-variables.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/linear-regression.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/machine-learning.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-data-access.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-introduction.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-matrix-operations.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-programming.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/matching-methods.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/mathematical-functions.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/maximum-likelihood.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/missing-data-handling.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/nonparametric-methods.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/panel-data.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/programming-basics.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/regression-discontinuity.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/sample-selection.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/sem-factor-analysis.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/spatial-analysis.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/string-functions.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/survey-data-analysis.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/survival-analysis.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/tables-reporting.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/time-series.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/treatment-effects.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/variables-operators.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/workflow-best-practices.md +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/smcl_parser.py +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/utils.py +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli.egg-info/SOURCES.txt +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli.egg-info/dependency_links.txt +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli.egg-info/entry_points.txt +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli.egg-info/requires.txt +0 -0
- {stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -18,13 +18,19 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any, Dict, Optional
|
|
19
19
|
|
|
20
20
|
_STATE_DIR = os.path.join(os.path.expanduser("~"), ".stata-cli")
|
|
21
|
-
_PID_FILE = os.path.join(_STATE_DIR, "daemon.pid")
|
|
22
|
-
_SOCK_FILE = os.path.join(_STATE_DIR, "daemon.sock")
|
|
23
21
|
_IS_WINDOWS = platform.system() == "Windows"
|
|
24
22
|
_DEFAULT_PORT = 4718
|
|
25
23
|
_IDLE_TIMEOUT = 3600 # 1 hour
|
|
26
24
|
|
|
27
25
|
|
|
26
|
+
def _pid_file(session: str = "default") -> str:
|
|
27
|
+
return os.path.join(_STATE_DIR, f"daemon-{session}.pid")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sock_file(session: str = "default") -> str:
|
|
31
|
+
return os.path.join(_STATE_DIR, f"daemon-{session}.sock")
|
|
32
|
+
|
|
33
|
+
|
|
28
34
|
# ── wire protocol: 4-byte big-endian length prefix + JSON ────────────────
|
|
29
35
|
|
|
30
36
|
def _send_msg(sock: socket.socket, obj: Any) -> None:
|
|
@@ -57,11 +63,13 @@ class DaemonServer:
|
|
|
57
63
|
"""Single-threaded daemon that wraps a StataEngine."""
|
|
58
64
|
|
|
59
65
|
def __init__(self, stata_path: str, edition: str = "mp",
|
|
60
|
-
graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT
|
|
66
|
+
graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT,
|
|
67
|
+
session: str = "default"):
|
|
61
68
|
self.stata_path = stata_path
|
|
62
69
|
self.edition = edition
|
|
63
70
|
self.graphs_dir = graphs_dir
|
|
64
71
|
self.idle_timeout = idle_timeout
|
|
72
|
+
self.session = session
|
|
65
73
|
self._engine = None
|
|
66
74
|
self._running = False
|
|
67
75
|
self._start_time = time.time()
|
|
@@ -77,6 +85,7 @@ class DaemonServer:
|
|
|
77
85
|
os.makedirs(_STATE_DIR, exist_ok=True)
|
|
78
86
|
|
|
79
87
|
sock = self._create_listener()
|
|
88
|
+
self._port = sock.getsockname()[1] if _IS_WINDOWS else 0
|
|
80
89
|
self._write_pid()
|
|
81
90
|
self._running = True
|
|
82
91
|
|
|
@@ -113,16 +122,30 @@ class DaemonServer:
|
|
|
113
122
|
if _IS_WINDOWS:
|
|
114
123
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
115
124
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
116
|
-
|
|
125
|
+
port = self._find_free_port()
|
|
126
|
+
sock.bind(("127.0.0.1", port))
|
|
117
127
|
else:
|
|
118
|
-
|
|
119
|
-
|
|
128
|
+
sock_path = _sock_file(self.session)
|
|
129
|
+
if os.path.exists(sock_path):
|
|
130
|
+
os.unlink(sock_path)
|
|
120
131
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
121
|
-
sock.bind(
|
|
132
|
+
sock.bind(sock_path)
|
|
122
133
|
sock.listen(4)
|
|
123
134
|
sock.setblocking(False)
|
|
124
135
|
return sock
|
|
125
136
|
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _find_free_port() -> int:
|
|
139
|
+
for port in range(_DEFAULT_PORT, _DEFAULT_PORT + 200):
|
|
140
|
+
try:
|
|
141
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
142
|
+
s.bind(("127.0.0.1", port))
|
|
143
|
+
s.close()
|
|
144
|
+
return port
|
|
145
|
+
except OSError:
|
|
146
|
+
continue
|
|
147
|
+
raise RuntimeError("No free port found")
|
|
148
|
+
|
|
126
149
|
def _handle_connection(self, conn: socket.socket) -> None:
|
|
127
150
|
conn.settimeout(600.0)
|
|
128
151
|
msg = _recv_msg(conn)
|
|
@@ -216,18 +239,19 @@ class DaemonServer:
|
|
|
216
239
|
return asdict(result)
|
|
217
240
|
|
|
218
241
|
def _write_pid(self) -> None:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
242
|
+
sock_path = _sock_file(self.session)
|
|
243
|
+
addr = f"127.0.0.1:{self._port}" if _IS_WINDOWS else sock_path
|
|
244
|
+
with open(_pid_file(self.session), "w") as fh:
|
|
245
|
+
json.dump({"pid": os.getpid(), "address": addr, "started": time.time(), "session": self.session}, fh)
|
|
222
246
|
|
|
223
247
|
def _cleanup(self) -> None:
|
|
224
248
|
try:
|
|
225
|
-
os.unlink(
|
|
249
|
+
os.unlink(_pid_file(self.session))
|
|
226
250
|
except OSError:
|
|
227
251
|
pass
|
|
228
252
|
if not _IS_WINDOWS:
|
|
229
253
|
try:
|
|
230
|
-
os.unlink(
|
|
254
|
+
os.unlink(_sock_file(self.session))
|
|
231
255
|
except OSError:
|
|
232
256
|
pass
|
|
233
257
|
|
|
@@ -236,9 +260,10 @@ class DaemonServer:
|
|
|
236
260
|
|
|
237
261
|
|
|
238
262
|
def run_daemon_server(stata_path: str, edition: str = "mp",
|
|
239
|
-
graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT
|
|
263
|
+
graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT,
|
|
264
|
+
session: str = "default") -> None:
|
|
240
265
|
"""Entry point called in the daemon subprocess."""
|
|
241
|
-
server = DaemonServer(stata_path, edition, graphs_dir=graphs_dir, idle_timeout=idle_timeout)
|
|
266
|
+
server = DaemonServer(stata_path, edition, graphs_dir=graphs_dir, idle_timeout=idle_timeout, session=session)
|
|
242
267
|
server.serve()
|
|
243
268
|
|
|
244
269
|
|
|
@@ -249,14 +274,16 @@ def run_daemon_server(stata_path: str, edition: str = "mp",
|
|
|
249
274
|
class DaemonClient:
|
|
250
275
|
"""Connects to a running daemon over the Unix socket / TCP port."""
|
|
251
276
|
|
|
252
|
-
def __init__(self) -> None:
|
|
277
|
+
def __init__(self, session: str = "default") -> None:
|
|
278
|
+
self.session = session
|
|
253
279
|
self._sock: Optional[socket.socket] = None
|
|
254
280
|
|
|
255
281
|
def is_running(self) -> bool:
|
|
256
|
-
|
|
282
|
+
pid_path = _pid_file(self.session)
|
|
283
|
+
if not os.path.isfile(pid_path):
|
|
257
284
|
return False
|
|
258
285
|
try:
|
|
259
|
-
with open(
|
|
286
|
+
with open(pid_path) as fh:
|
|
260
287
|
info = json.load(fh)
|
|
261
288
|
pid = info["pid"]
|
|
262
289
|
os.kill(pid, 0)
|
|
@@ -298,30 +325,33 @@ class DaemonClient:
|
|
|
298
325
|
self._sock = None
|
|
299
326
|
|
|
300
327
|
def _read_pid(self) -> Optional[Dict]:
|
|
301
|
-
|
|
328
|
+
pid_path = _pid_file(self.session)
|
|
329
|
+
if not os.path.isfile(pid_path):
|
|
302
330
|
return None
|
|
303
331
|
try:
|
|
304
|
-
with open(
|
|
332
|
+
with open(pid_path) as fh:
|
|
305
333
|
return json.load(fh)
|
|
306
334
|
except (json.JSONDecodeError, OSError):
|
|
307
335
|
return None
|
|
308
336
|
|
|
309
337
|
|
|
310
338
|
def start_daemon(stata_path: str, edition: str = "mp",
|
|
311
|
-
graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT
|
|
339
|
+
graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT,
|
|
340
|
+
session: str = "default") -> bool:
|
|
312
341
|
"""Launch daemon as a detached background process. Returns True on success."""
|
|
313
|
-
client = DaemonClient()
|
|
342
|
+
client = DaemonClient(session)
|
|
314
343
|
if client.is_running():
|
|
315
344
|
return True
|
|
316
345
|
|
|
317
346
|
os.makedirs(_STATE_DIR, exist_ok=True)
|
|
318
|
-
log_file = os.path.join(_STATE_DIR, "daemon.log")
|
|
347
|
+
log_file = os.path.join(_STATE_DIR, f"daemon-{session}.log")
|
|
319
348
|
|
|
320
349
|
args = [
|
|
321
350
|
sys.executable, "-m", "stata_cli.daemon",
|
|
322
351
|
"--stata-path", stata_path,
|
|
323
352
|
"--edition", edition,
|
|
324
353
|
"--idle-timeout", str(int(idle_timeout)),
|
|
354
|
+
"--session", session,
|
|
325
355
|
]
|
|
326
356
|
if graphs_dir:
|
|
327
357
|
args += ["--graphs-dir", graphs_dir]
|
|
@@ -345,9 +375,9 @@ def start_daemon(stata_path: str, edition: str = "mp",
|
|
|
345
375
|
return False
|
|
346
376
|
|
|
347
377
|
|
|
348
|
-
def stop_daemon() -> bool:
|
|
378
|
+
def stop_daemon(session: str = "default") -> bool:
|
|
349
379
|
"""Ask the daemon to shut down gracefully."""
|
|
350
|
-
client = DaemonClient()
|
|
380
|
+
client = DaemonClient(session)
|
|
351
381
|
if not client.is_running():
|
|
352
382
|
return True
|
|
353
383
|
if client.connect():
|
|
@@ -362,7 +392,7 @@ def stop_daemon() -> bool:
|
|
|
362
392
|
return True
|
|
363
393
|
# Force kill
|
|
364
394
|
try:
|
|
365
|
-
with open(
|
|
395
|
+
with open(_pid_file(session)) as fh:
|
|
366
396
|
pid = json.load(fh)["pid"]
|
|
367
397
|
os.kill(pid, signal.SIGKILL if not _IS_WINDOWS else signal.SIGTERM)
|
|
368
398
|
except Exception:
|
|
@@ -370,9 +400,43 @@ def stop_daemon() -> bool:
|
|
|
370
400
|
return True
|
|
371
401
|
|
|
372
402
|
|
|
373
|
-
def
|
|
374
|
-
"""
|
|
375
|
-
|
|
403
|
+
def stop_all_daemons() -> int:
|
|
404
|
+
"""Stop all running daemon sessions. Returns count stopped."""
|
|
405
|
+
import glob
|
|
406
|
+
count = 0
|
|
407
|
+
for pid_path in glob.glob(os.path.join(_STATE_DIR, "daemon-*.pid")):
|
|
408
|
+
name = os.path.basename(pid_path)
|
|
409
|
+
session = name[len("daemon-"):-len(".pid")]
|
|
410
|
+
stop_daemon(session)
|
|
411
|
+
count += 1
|
|
412
|
+
return count
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def list_sessions() -> list:
|
|
416
|
+
"""Return list of running session info dicts."""
|
|
417
|
+
import glob
|
|
418
|
+
sessions = []
|
|
419
|
+
for pid_path in glob.glob(os.path.join(_STATE_DIR, "daemon-*.pid")):
|
|
420
|
+
name = os.path.basename(pid_path)
|
|
421
|
+
session = name[len("daemon-"):-len(".pid")]
|
|
422
|
+
client = DaemonClient(session)
|
|
423
|
+
if not client.is_running():
|
|
424
|
+
continue
|
|
425
|
+
info = {"session": session}
|
|
426
|
+
if client.connect():
|
|
427
|
+
try:
|
|
428
|
+
status = client.send("status")
|
|
429
|
+
info.update(status)
|
|
430
|
+
except Exception:
|
|
431
|
+
pass
|
|
432
|
+
client.close()
|
|
433
|
+
sessions.append(info)
|
|
434
|
+
return sessions
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def daemon_status(session: str = "default") -> Optional[Dict[str, Any]]:
|
|
438
|
+
"""Query a running daemon's status. Returns None if not running."""
|
|
439
|
+
client = DaemonClient(session)
|
|
376
440
|
if not client.is_running():
|
|
377
441
|
return None
|
|
378
442
|
if not client.connect():
|
|
@@ -394,5 +458,6 @@ if __name__ == "__main__":
|
|
|
394
458
|
parser.add_argument("--edition", default="mp")
|
|
395
459
|
parser.add_argument("--graphs-dir", default=None)
|
|
396
460
|
parser.add_argument("--idle-timeout", type=float, default=_IDLE_TIMEOUT)
|
|
461
|
+
parser.add_argument("--session", default="default")
|
|
397
462
|
args = parser.parse_args()
|
|
398
|
-
run_daemon_server(args.stata_path, args.edition, args.graphs_dir, args.idle_timeout)
|
|
463
|
+
run_daemon_server(args.stata_path, args.edition, args.graphs_dir, args.idle_timeout, session=args.session)
|
|
@@ -36,11 +36,12 @@ def _exit(code: int) -> None:
|
|
|
36
36
|
@click.option("--timeout", type=float, default=600.0, help="Execution timeout in seconds.")
|
|
37
37
|
@click.option("--max-tokens", type=int, default=0, help="Max output tokens (0=unlimited). Saves full output to file when exceeded.")
|
|
38
38
|
@click.option("--no-daemon", is_flag=True, default=False, help="Force direct execution, skip daemon.")
|
|
39
|
+
@click.option("--session", default="default", help="Daemon session name (for parallel sessions).")
|
|
39
40
|
@click.option("--graphs-dir", envvar="STATA_CLI_GRAPHS_DIR", default=None, help="Graph export directory.")
|
|
40
41
|
@click.option("--graph-format", type=click.Choice(["png", "svg", "pdf"], case_sensitive=False), default="png", help="Graph export format.")
|
|
41
42
|
@click.option("--log", "log_file", default=None, help="Save Stata output to a log file.")
|
|
42
43
|
@click.pass_context
|
|
43
|
-
def cli(ctx, stata_path, edition, compact, use_json, timeout, max_tokens, no_daemon, graphs_dir, graph_format, log_file):
|
|
44
|
+
def cli(ctx, stata_path, edition, compact, use_json, timeout, max_tokens, no_daemon, session, graphs_dir, graph_format, log_file):
|
|
44
45
|
"""Command-line interface for Stata."""
|
|
45
46
|
ctx.ensure_object(dict)
|
|
46
47
|
ctx.obj["stata_path"] = stata_path
|
|
@@ -50,6 +51,7 @@ def cli(ctx, stata_path, edition, compact, use_json, timeout, max_tokens, no_dae
|
|
|
50
51
|
ctx.obj["timeout"] = timeout
|
|
51
52
|
ctx.obj["max_tokens"] = max_tokens
|
|
52
53
|
ctx.obj["no_daemon"] = no_daemon
|
|
54
|
+
ctx.obj["session"] = session
|
|
53
55
|
ctx.obj["graphs_dir"] = graphs_dir
|
|
54
56
|
ctx.obj["graph_format"] = graph_format
|
|
55
57
|
ctx.obj["log_file"] = log_file
|
|
@@ -75,7 +77,7 @@ def _try_daemon(ctx, cmd_type: str, payload: dict) -> Result | None:
|
|
|
75
77
|
return None
|
|
76
78
|
try:
|
|
77
79
|
from .daemon import DaemonClient
|
|
78
|
-
client = DaemonClient()
|
|
80
|
+
client = DaemonClient(ctx.obj.get("session", "default"))
|
|
79
81
|
if not client.is_running():
|
|
80
82
|
return None
|
|
81
83
|
if not client.connect():
|
|
@@ -100,7 +102,7 @@ def _try_daemon_dict(ctx, cmd_type: str, payload: dict) -> dict | None:
|
|
|
100
102
|
return None
|
|
101
103
|
try:
|
|
102
104
|
from .daemon import DaemonClient
|
|
103
|
-
client = DaemonClient()
|
|
105
|
+
client = DaemonClient(ctx.obj.get("session", "default"))
|
|
104
106
|
if not client.is_running():
|
|
105
107
|
return None
|
|
106
108
|
if not client.connect():
|
|
@@ -222,7 +224,7 @@ def data_cmd(ctx, if_condition, rows):
|
|
|
222
224
|
# Try daemon first
|
|
223
225
|
try:
|
|
224
226
|
from .daemon import DaemonClient
|
|
225
|
-
client = DaemonClient()
|
|
227
|
+
client = DaemonClient(ctx.obj.get("session", "default"))
|
|
226
228
|
if not ctx.obj.get("no_daemon") and client.is_running() and client.connect():
|
|
227
229
|
resp = client.send("get_data", {"if_condition": if_condition, "max_rows": rows})
|
|
228
230
|
client.close()
|
|
@@ -266,7 +268,7 @@ def stop_cmd(ctx):
|
|
|
266
268
|
"""Interrupt a running Stata command (daemon mode)."""
|
|
267
269
|
try:
|
|
268
270
|
from .daemon import DaemonClient
|
|
269
|
-
client = DaemonClient()
|
|
271
|
+
client = DaemonClient(ctx.obj.get("session", "default"))
|
|
270
272
|
if client.is_running() and client.connect():
|
|
271
273
|
resp = client.send("stop")
|
|
272
274
|
client.close()
|
|
@@ -408,7 +410,7 @@ def frame_cmd(ctx):
|
|
|
408
410
|
|
|
409
411
|
# ── Skill command ────────────────────────────────────────────────────────
|
|
410
412
|
|
|
411
|
-
@cli.command("skill")
|
|
413
|
+
@cli.command("skill", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
|
|
412
414
|
@click.argument("topic", required=False, default=None)
|
|
413
415
|
@click.option("--list", "list_topics", is_flag=True, default=False, help="List all available topics.")
|
|
414
416
|
@click.pass_context
|
|
@@ -462,48 +464,68 @@ def daemon_start(ctx, idle_timeout):
|
|
|
462
464
|
click.echo("Error: Stata installation not found.", err=True)
|
|
463
465
|
_exit(EXIT_INIT_FAILURE)
|
|
464
466
|
|
|
467
|
+
session = ctx.obj["session"]
|
|
465
468
|
from .daemon import start_daemon, DaemonClient
|
|
466
|
-
client = DaemonClient()
|
|
469
|
+
client = DaemonClient(session)
|
|
467
470
|
if client.is_running():
|
|
468
|
-
click.echo("Daemon already running.")
|
|
471
|
+
click.echo(f"Daemon '{session}' already running.")
|
|
469
472
|
return
|
|
470
473
|
|
|
471
|
-
click.echo("Starting daemon...")
|
|
472
|
-
ok = start_daemon(stata_path, ctx.obj["edition"], graphs_dir=ctx.obj.get("graphs_dir"), idle_timeout=idle_timeout)
|
|
474
|
+
click.echo(f"Starting daemon '{session}'...")
|
|
475
|
+
ok = start_daemon(stata_path, ctx.obj["edition"], graphs_dir=ctx.obj.get("graphs_dir"), idle_timeout=idle_timeout, session=session)
|
|
473
476
|
if ok:
|
|
474
|
-
click.echo("Daemon started.")
|
|
477
|
+
click.echo(f"Daemon '{session}' started.")
|
|
475
478
|
else:
|
|
476
479
|
click.echo("Failed to start daemon.", err=True)
|
|
477
480
|
_exit(EXIT_INIT_FAILURE)
|
|
478
481
|
|
|
479
482
|
|
|
480
483
|
@daemon.command("stop")
|
|
481
|
-
|
|
484
|
+
@click.option("--all", "stop_all", is_flag=True, default=False, help="Stop all running sessions.")
|
|
485
|
+
@click.pass_context
|
|
486
|
+
def daemon_stop(ctx, stop_all):
|
|
482
487
|
"""Stop the Stata daemon."""
|
|
483
|
-
from .daemon import stop_daemon, DaemonClient
|
|
484
|
-
|
|
488
|
+
from .daemon import stop_daemon as _stop, stop_all_daemons, DaemonClient
|
|
489
|
+
if stop_all:
|
|
490
|
+
count = stop_all_daemons()
|
|
491
|
+
click.echo(f"Stopped {count} session(s).")
|
|
492
|
+
return
|
|
493
|
+
session = ctx.obj["session"]
|
|
494
|
+
client = DaemonClient(session)
|
|
485
495
|
if not client.is_running():
|
|
486
|
-
click.echo("Daemon not running.")
|
|
496
|
+
click.echo(f"Daemon '{session}' not running.")
|
|
487
497
|
return
|
|
488
|
-
click.echo("Stopping daemon...")
|
|
489
|
-
|
|
490
|
-
click.echo("Daemon stopped.")
|
|
498
|
+
click.echo(f"Stopping daemon '{session}'...")
|
|
499
|
+
_stop(session)
|
|
500
|
+
click.echo(f"Daemon '{session}' stopped.")
|
|
491
501
|
|
|
492
502
|
|
|
493
503
|
@daemon.command("status")
|
|
494
|
-
|
|
504
|
+
@click.pass_context
|
|
505
|
+
def daemon_status_cmd(ctx):
|
|
495
506
|
"""Show daemon status."""
|
|
496
|
-
from .daemon import daemon_status
|
|
497
|
-
|
|
498
|
-
if
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
+
from .daemon import daemon_status, list_sessions
|
|
508
|
+
session = ctx.obj["session"]
|
|
509
|
+
if session == "default":
|
|
510
|
+
sessions = list_sessions()
|
|
511
|
+
if not sessions:
|
|
512
|
+
click.echo("No daemons running.")
|
|
513
|
+
return
|
|
514
|
+
for s in sessions:
|
|
515
|
+
uptime = s.get("uptime", 0)
|
|
516
|
+
idle = s.get("idle", 0)
|
|
517
|
+
click.echo(f"[{s['session']}] PID {s.get('pid', '?')} | uptime {int(uptime)}s | idle {int(idle)}s")
|
|
518
|
+
else:
|
|
519
|
+
info = daemon_status(session)
|
|
520
|
+
if not info:
|
|
521
|
+
click.echo(f"Daemon '{session}' not running.")
|
|
522
|
+
return
|
|
523
|
+
uptime = info.get("uptime", 0)
|
|
524
|
+
idle = info.get("idle", 0)
|
|
525
|
+
click.echo(f"Daemon '{session}' running (PID {info.get('pid', '?')})")
|
|
526
|
+
click.echo(f" Stata: {info.get('stata_path', '?')} ({info.get('edition', '?')})")
|
|
527
|
+
click.echo(f" Uptime: {int(uptime)}s")
|
|
528
|
+
click.echo(f" Idle: {int(idle)}s")
|
|
507
529
|
|
|
508
530
|
|
|
509
531
|
@daemon.command("restart")
|
|
@@ -511,21 +533,22 @@ def daemon_status_cmd():
|
|
|
511
533
|
@click.pass_context
|
|
512
534
|
def daemon_restart(ctx, idle_timeout):
|
|
513
535
|
"""Restart the Stata daemon."""
|
|
514
|
-
|
|
515
|
-
|
|
536
|
+
session = ctx.obj["session"]
|
|
537
|
+
from .daemon import stop_daemon as _stop, start_daemon, DaemonClient
|
|
538
|
+
client = DaemonClient(session)
|
|
516
539
|
if client.is_running():
|
|
517
|
-
click.echo("Stopping daemon...")
|
|
518
|
-
|
|
540
|
+
click.echo(f"Stopping daemon '{session}'...")
|
|
541
|
+
_stop(session)
|
|
519
542
|
|
|
520
543
|
stata_path = ctx.obj["stata_path"] or detect_stata_path()
|
|
521
544
|
if not stata_path:
|
|
522
545
|
click.echo("Error: Stata installation not found.", err=True)
|
|
523
546
|
_exit(EXIT_INIT_FAILURE)
|
|
524
547
|
|
|
525
|
-
click.echo("Starting daemon...")
|
|
526
|
-
ok = start_daemon(stata_path, ctx.obj["edition"], graphs_dir=ctx.obj.get("graphs_dir"), idle_timeout=idle_timeout)
|
|
548
|
+
click.echo(f"Starting daemon '{session}'...")
|
|
549
|
+
ok = start_daemon(stata_path, ctx.obj["edition"], graphs_dir=ctx.obj.get("graphs_dir"), idle_timeout=idle_timeout, session=session)
|
|
527
550
|
if ok:
|
|
528
|
-
click.echo("Daemon restarted.")
|
|
551
|
+
click.echo(f"Daemon '{session}' restarted.")
|
|
529
552
|
else:
|
|
530
553
|
click.echo("Failed to restart daemon.", err=True)
|
|
531
554
|
_exit(EXIT_INIT_FAILURE)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.4.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/basics-getting-started.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/descriptive-statistics.md
RENAMED
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/difference-in-differences.md
RENAMED
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/external-tools-integration.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/limited-dependent-variables.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-matrix-operations.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/mathematical-functions.md
RENAMED
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/missing-data-handling.md
RENAMED
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/nonparametric-methods.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/regression-discontinuity.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stata_cli-0.4.1 → stata_cli-0.5.0}/src/stata_cli/skills/references/workflow-best-practices.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|