stata-cli 0.4.2__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.
Files changed (79) hide show
  1. {stata_cli-0.4.2 → stata_cli-0.5.0}/PKG-INFO +1 -1
  2. {stata_cli-0.4.2 → stata_cli-0.5.0}/pyproject.toml +1 -1
  3. stata_cli-0.5.0/src/stata_cli/__init__.py +1 -0
  4. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/daemon.py +94 -29
  5. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/main.py +59 -36
  6. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli.egg-info/PKG-INFO +1 -1
  7. stata_cli-0.4.2/src/stata_cli/__init__.py +0 -1
  8. {stata_cli-0.4.2 → stata_cli-0.5.0}/README.md +0 -0
  9. {stata_cli-0.4.2 → stata_cli-0.5.0}/setup.cfg +0 -0
  10. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/__main__.py +0 -0
  11. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/engine.py +0 -0
  12. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/graph_artifacts.py +0 -0
  13. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/output_filter.py +0 -0
  14. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skill_registry.py +0 -0
  15. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/overview.md +0 -0
  16. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/asdoc.md +0 -0
  17. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/binsreg.md +0 -0
  18. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/coefplot.md +0 -0
  19. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/data-manipulation.md +0 -0
  20. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/diagnostics.md +0 -0
  21. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/did.md +0 -0
  22. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/estout.md +0 -0
  23. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/event-study.md +0 -0
  24. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/graph-schemes.md +0 -0
  25. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/ivreg2.md +0 -0
  26. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/nprobust.md +0 -0
  27. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/outreg2.md +0 -0
  28. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/package-management.md +0 -0
  29. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/psmatch2.md +0 -0
  30. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/rdrobust.md +0 -0
  31. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/reghdfe.md +0 -0
  32. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/synth.md +0 -0
  33. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/tabout.md +0 -0
  34. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/winsor.md +0 -0
  35. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/packages/xtabond2.md +0 -0
  36. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/advanced-programming.md +0 -0
  37. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/basics-getting-started.md +0 -0
  38. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/bootstrap-simulation.md +0 -0
  39. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/data-import-export.md +0 -0
  40. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/data-management.md +0 -0
  41. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/date-time-functions.md +0 -0
  42. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/descriptive-statistics.md +0 -0
  43. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/difference-in-differences.md +0 -0
  44. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/external-tools-integration.md +0 -0
  45. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/gmm-estimation.md +0 -0
  46. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/graphics.md +0 -0
  47. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/limited-dependent-variables.md +0 -0
  48. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/linear-regression.md +0 -0
  49. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/machine-learning.md +0 -0
  50. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-data-access.md +0 -0
  51. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-introduction.md +0 -0
  52. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-matrix-operations.md +0 -0
  53. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/mata-programming.md +0 -0
  54. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/matching-methods.md +0 -0
  55. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/mathematical-functions.md +0 -0
  56. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/maximum-likelihood.md +0 -0
  57. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/missing-data-handling.md +0 -0
  58. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/nonparametric-methods.md +0 -0
  59. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/panel-data.md +0 -0
  60. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/programming-basics.md +0 -0
  61. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/regression-discontinuity.md +0 -0
  62. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/sample-selection.md +0 -0
  63. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/sem-factor-analysis.md +0 -0
  64. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/spatial-analysis.md +0 -0
  65. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/string-functions.md +0 -0
  66. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/survey-data-analysis.md +0 -0
  67. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/survival-analysis.md +0 -0
  68. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/tables-reporting.md +0 -0
  69. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/time-series.md +0 -0
  70. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/treatment-effects.md +0 -0
  71. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/variables-operators.md +0 -0
  72. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/skills/references/workflow-best-practices.md +0 -0
  73. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/smcl_parser.py +0 -0
  74. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli/utils.py +0 -0
  75. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli.egg-info/SOURCES.txt +0 -0
  76. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli.egg-info/dependency_links.txt +0 -0
  77. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli.egg-info/entry_points.txt +0 -0
  78. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli.egg-info/requires.txt +0 -0
  79. {stata_cli-0.4.2 → stata_cli-0.5.0}/src/stata_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stata-cli
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Command-line interface for running Stata commands via PyStata
5
5
  License: MIT
6
6
  Keywords: stata,cli,statistics,data-science
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stata-cli"
3
- version = "0.4.2"
3
+ version = "0.5.0"
4
4
  description = "Command-line interface for running Stata commands via PyStata"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -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
- sock.bind(("127.0.0.1", _DEFAULT_PORT))
125
+ port = self._find_free_port()
126
+ sock.bind(("127.0.0.1", port))
117
127
  else:
118
- if os.path.exists(_SOCK_FILE):
119
- os.unlink(_SOCK_FILE)
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(_SOCK_FILE)
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
- addr = f"127.0.0.1:{_DEFAULT_PORT}" if _IS_WINDOWS else _SOCK_FILE
220
- with open(_PID_FILE, "w") as fh:
221
- json.dump({"pid": os.getpid(), "address": addr, "started": time.time()}, fh)
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(_PID_FILE)
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(_SOCK_FILE)
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) -> None:
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
- if not os.path.isfile(_PID_FILE):
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(_PID_FILE) as fh:
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
- if not os.path.isfile(_PID_FILE):
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(_PID_FILE) as fh:
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) -> bool:
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(_PID_FILE) as fh:
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 daemon_status() -> Optional[Dict[str, Any]]:
374
- """Query the running daemon's status. Returns None if not running."""
375
- client = DaemonClient()
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()
@@ -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
- def daemon_stop():
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
- client = DaemonClient()
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
- stop_daemon()
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
- def daemon_status_cmd():
504
+ @click.pass_context
505
+ def daemon_status_cmd(ctx):
495
506
  """Show daemon status."""
496
- from .daemon import daemon_status
497
- info = daemon_status()
498
- if not info:
499
- click.echo("Daemon not running.")
500
- return
501
- uptime = info.get("uptime", 0)
502
- idle = info.get("idle", 0)
503
- click.echo(f"Daemon running (PID {info.get('pid', '?')})")
504
- click.echo(f" Stata: {info.get('stata_path', '?')} ({info.get('edition', '?')})")
505
- click.echo(f" Uptime: {int(uptime)}s")
506
- click.echo(f" Idle: {int(idle)}s")
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
- from .daemon import stop_daemon, start_daemon, DaemonClient
515
- client = DaemonClient()
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
- stop_daemon()
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stata-cli
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Command-line interface for running Stata commands via PyStata
5
5
  License: MIT
6
6
  Keywords: stata,cli,statistics,data-science
@@ -1 +0,0 @@
1
- __version__ = "0.4.2"
File without changes
File without changes