stata-cli 0.4.2__tar.gz → 0.5.1__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.1}/PKG-INFO +23 -3
  2. {stata_cli-0.4.2 → stata_cli-0.5.1}/README.md +22 -2
  3. {stata_cli-0.4.2 → stata_cli-0.5.1}/pyproject.toml +1 -1
  4. stata_cli-0.5.1/src/stata_cli/__init__.py +1 -0
  5. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/daemon.py +95 -29
  6. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/main.py +59 -36
  7. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli.egg-info/PKG-INFO +23 -3
  8. stata_cli-0.4.2/src/stata_cli/__init__.py +0 -1
  9. {stata_cli-0.4.2 → stata_cli-0.5.1}/setup.cfg +0 -0
  10. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/__main__.py +0 -0
  11. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/engine.py +0 -0
  12. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/graph_artifacts.py +0 -0
  13. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/output_filter.py +0 -0
  14. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skill_registry.py +0 -0
  15. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/overview.md +0 -0
  16. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/asdoc.md +0 -0
  17. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/binsreg.md +0 -0
  18. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/coefplot.md +0 -0
  19. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/data-manipulation.md +0 -0
  20. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/diagnostics.md +0 -0
  21. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/did.md +0 -0
  22. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/estout.md +0 -0
  23. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/event-study.md +0 -0
  24. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/graph-schemes.md +0 -0
  25. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/ivreg2.md +0 -0
  26. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/nprobust.md +0 -0
  27. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/outreg2.md +0 -0
  28. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/package-management.md +0 -0
  29. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/psmatch2.md +0 -0
  30. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/rdrobust.md +0 -0
  31. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/reghdfe.md +0 -0
  32. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/synth.md +0 -0
  33. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/tabout.md +0 -0
  34. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/winsor.md +0 -0
  35. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/packages/xtabond2.md +0 -0
  36. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/advanced-programming.md +0 -0
  37. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/basics-getting-started.md +0 -0
  38. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/bootstrap-simulation.md +0 -0
  39. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/data-import-export.md +0 -0
  40. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/data-management.md +0 -0
  41. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/date-time-functions.md +0 -0
  42. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/descriptive-statistics.md +0 -0
  43. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/difference-in-differences.md +0 -0
  44. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/external-tools-integration.md +0 -0
  45. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/gmm-estimation.md +0 -0
  46. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/graphics.md +0 -0
  47. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/limited-dependent-variables.md +0 -0
  48. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/linear-regression.md +0 -0
  49. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/machine-learning.md +0 -0
  50. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/mata-data-access.md +0 -0
  51. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/mata-introduction.md +0 -0
  52. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/mata-matrix-operations.md +0 -0
  53. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/mata-programming.md +0 -0
  54. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/matching-methods.md +0 -0
  55. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/mathematical-functions.md +0 -0
  56. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/maximum-likelihood.md +0 -0
  57. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/missing-data-handling.md +0 -0
  58. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/nonparametric-methods.md +0 -0
  59. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/panel-data.md +0 -0
  60. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/programming-basics.md +0 -0
  61. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/regression-discontinuity.md +0 -0
  62. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/sample-selection.md +0 -0
  63. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/sem-factor-analysis.md +0 -0
  64. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/spatial-analysis.md +0 -0
  65. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/string-functions.md +0 -0
  66. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/survey-data-analysis.md +0 -0
  67. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/survival-analysis.md +0 -0
  68. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/tables-reporting.md +0 -0
  69. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/time-series.md +0 -0
  70. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/treatment-effects.md +0 -0
  71. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/variables-operators.md +0 -0
  72. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/skills/references/workflow-best-practices.md +0 -0
  73. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/smcl_parser.py +0 -0
  74. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli/utils.py +0 -0
  75. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli.egg-info/SOURCES.txt +0 -0
  76. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli.egg-info/dependency_links.txt +0 -0
  77. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli.egg-info/entry_points.txt +0 -0
  78. {stata_cli-0.4.2 → stata_cli-0.5.1}/src/stata_cli.egg-info/requires.txt +0 -0
  79. {stata_cli-0.4.2 → stata_cli-0.5.1}/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.1
4
4
  Summary: Command-line interface for running Stata commands via PyStata
5
5
  License: MIT
6
6
  Keywords: stata,cli,statistics,data-science
@@ -51,7 +51,7 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
51
51
  | **Frame Management** | List Stata frames and current working frame via `frame` |
52
52
  | **Help System** | Browse Stata help topics with SMCL-to-plain-text conversion |
53
53
  | **Graph Export** | Auto-detect and export graphs as PNG/SVG/PDF to `~/.stata-cli/graphs/` |
54
- | **Daemon Mode** | Persistent background process for sub-second execution via Unix socket |
54
+ | **Daemon Mode** | Persistent background process for sub-second execution; parallel sessions via `--session` |
55
55
  | **Output Control** | Compact mode, JSON output, token limit management, log file output |
56
56
  | **Interruption** | Send break signal to stop long-running commands |
57
57
  | **Skill Library** | Built-in Stata reference with 57 topics: syntax, econometrics, causal inference, packages |
@@ -290,13 +290,32 @@ stata-cli daemon stop # Shut down
290
290
  |---------|-------------|
291
291
  | `daemon start` | Start the background daemon process |
292
292
  | `daemon stop` | Graceful shutdown |
293
- | `daemon status` | Show PID, Stata path, uptime, idle time |
293
+ | `daemon stop --all` | Stop all running sessions |
294
+ | `daemon status` | Show all running sessions (PID, uptime, idle) |
294
295
  | `daemon restart` | Stop + start (clean Stata state) |
295
296
 
296
297
  Commands auto-route through the daemon when it is running. Use `--no-daemon` to force direct execution.
297
298
 
298
299
  The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle-timeout`).
299
300
 
301
+ ### Parallel Sessions
302
+
303
+ Run multiple independent Stata instances — like opening multiple Stata windows:
304
+
305
+ ```bash
306
+ # Start named sessions
307
+ stata-cli --session proj_a daemon start
308
+ stata-cli --session proj_b daemon start
309
+
310
+ # Each session has its own data, estimates, and macros
311
+ stata-cli --session proj_a run "use project_a.dta, clear"
312
+ stata-cli --session proj_b run "use project_b.dta, clear"
313
+
314
+ # Route any command to a specific session
315
+ stata-cli --session proj_a run "regress price mpg weight"
316
+ stata-cli --session proj_b return e
317
+ ```
318
+
300
319
  ## Advanced Usage
301
320
 
302
321
  ### Global Options
@@ -305,6 +324,7 @@ The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle
305
324
  |--------|-------------|---------|
306
325
  | `--stata-path PATH` | Stata installation directory | auto-detected |
307
326
  | `--edition [mp\|se\|be]` | Stata edition | `mp` |
327
+ | `--session NAME` | Daemon session name (for parallel sessions) | `default` |
308
328
  | `--compact` | Strip verbose output noise | off |
309
329
  | `--json` | Structured JSON output | off |
310
330
  | `--timeout SECONDS` | Execution timeout | 600 |
@@ -38,7 +38,7 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
38
38
  | **Frame Management** | List Stata frames and current working frame via `frame` |
39
39
  | **Help System** | Browse Stata help topics with SMCL-to-plain-text conversion |
40
40
  | **Graph Export** | Auto-detect and export graphs as PNG/SVG/PDF to `~/.stata-cli/graphs/` |
41
- | **Daemon Mode** | Persistent background process for sub-second execution via Unix socket |
41
+ | **Daemon Mode** | Persistent background process for sub-second execution; parallel sessions via `--session` |
42
42
  | **Output Control** | Compact mode, JSON output, token limit management, log file output |
43
43
  | **Interruption** | Send break signal to stop long-running commands |
44
44
  | **Skill Library** | Built-in Stata reference with 57 topics: syntax, econometrics, causal inference, packages |
@@ -277,13 +277,32 @@ stata-cli daemon stop # Shut down
277
277
  |---------|-------------|
278
278
  | `daemon start` | Start the background daemon process |
279
279
  | `daemon stop` | Graceful shutdown |
280
- | `daemon status` | Show PID, Stata path, uptime, idle time |
280
+ | `daemon stop --all` | Stop all running sessions |
281
+ | `daemon status` | Show all running sessions (PID, uptime, idle) |
281
282
  | `daemon restart` | Stop + start (clean Stata state) |
282
283
 
283
284
  Commands auto-route through the daemon when it is running. Use `--no-daemon` to force direct execution.
284
285
 
285
286
  The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle-timeout`).
286
287
 
288
+ ### Parallel Sessions
289
+
290
+ Run multiple independent Stata instances — like opening multiple Stata windows:
291
+
292
+ ```bash
293
+ # Start named sessions
294
+ stata-cli --session proj_a daemon start
295
+ stata-cli --session proj_b daemon start
296
+
297
+ # Each session has its own data, estimates, and macros
298
+ stata-cli --session proj_a run "use project_a.dta, clear"
299
+ stata-cli --session proj_b run "use project_b.dta, clear"
300
+
301
+ # Route any command to a specific session
302
+ stata-cli --session proj_a run "regress price mpg weight"
303
+ stata-cli --session proj_b return e
304
+ ```
305
+
287
306
  ## Advanced Usage
288
307
 
289
308
  ### Global Options
@@ -292,6 +311,7 @@ The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle
292
311
  |--------|-------------|---------|
293
312
  | `--stata-path PATH` | Stata installation directory | auto-detected |
294
313
  | `--edition [mp\|se\|be]` | Stata edition | `mp` |
314
+ | `--session NAME` | Daemon session name (for parallel sessions) | `default` |
295
315
  | `--compact` | Strip verbose output noise | off |
296
316
  | `--json` | Structured JSON output | off |
297
317
  | `--timeout SECONDS` | Execution timeout | 600 |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stata-cli"
3
- version = "0.4.2"
3
+ version = "0.5.1"
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.1"
@@ -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,31 @@ 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
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
141
+ try:
142
+ s.bind(("127.0.0.1", port))
143
+ s.close()
144
+ return port
145
+ except OSError:
146
+ s.close()
147
+ continue
148
+ raise RuntimeError("No free port found")
149
+
126
150
  def _handle_connection(self, conn: socket.socket) -> None:
127
151
  conn.settimeout(600.0)
128
152
  msg = _recv_msg(conn)
@@ -216,18 +240,19 @@ class DaemonServer:
216
240
  return asdict(result)
217
241
 
218
242
  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)
243
+ sock_path = _sock_file(self.session)
244
+ addr = f"127.0.0.1:{self._port}" if _IS_WINDOWS else sock_path
245
+ with open(_pid_file(self.session), "w") as fh:
246
+ json.dump({"pid": os.getpid(), "address": addr, "started": time.time(), "session": self.session}, fh)
222
247
 
223
248
  def _cleanup(self) -> None:
224
249
  try:
225
- os.unlink(_PID_FILE)
250
+ os.unlink(_pid_file(self.session))
226
251
  except OSError:
227
252
  pass
228
253
  if not _IS_WINDOWS:
229
254
  try:
230
- os.unlink(_SOCK_FILE)
255
+ os.unlink(_sock_file(self.session))
231
256
  except OSError:
232
257
  pass
233
258
 
@@ -236,9 +261,10 @@ class DaemonServer:
236
261
 
237
262
 
238
263
  def run_daemon_server(stata_path: str, edition: str = "mp",
239
- graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT) -> None:
264
+ graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT,
265
+ session: str = "default") -> None:
240
266
  """Entry point called in the daemon subprocess."""
241
- server = DaemonServer(stata_path, edition, graphs_dir=graphs_dir, idle_timeout=idle_timeout)
267
+ server = DaemonServer(stata_path, edition, graphs_dir=graphs_dir, idle_timeout=idle_timeout, session=session)
242
268
  server.serve()
243
269
 
244
270
 
@@ -249,14 +275,16 @@ def run_daemon_server(stata_path: str, edition: str = "mp",
249
275
  class DaemonClient:
250
276
  """Connects to a running daemon over the Unix socket / TCP port."""
251
277
 
252
- def __init__(self) -> None:
278
+ def __init__(self, session: str = "default") -> None:
279
+ self.session = session
253
280
  self._sock: Optional[socket.socket] = None
254
281
 
255
282
  def is_running(self) -> bool:
256
- if not os.path.isfile(_PID_FILE):
283
+ pid_path = _pid_file(self.session)
284
+ if not os.path.isfile(pid_path):
257
285
  return False
258
286
  try:
259
- with open(_PID_FILE) as fh:
287
+ with open(pid_path) as fh:
260
288
  info = json.load(fh)
261
289
  pid = info["pid"]
262
290
  os.kill(pid, 0)
@@ -298,30 +326,33 @@ class DaemonClient:
298
326
  self._sock = None
299
327
 
300
328
  def _read_pid(self) -> Optional[Dict]:
301
- if not os.path.isfile(_PID_FILE):
329
+ pid_path = _pid_file(self.session)
330
+ if not os.path.isfile(pid_path):
302
331
  return None
303
332
  try:
304
- with open(_PID_FILE) as fh:
333
+ with open(pid_path) as fh:
305
334
  return json.load(fh)
306
335
  except (json.JSONDecodeError, OSError):
307
336
  return None
308
337
 
309
338
 
310
339
  def start_daemon(stata_path: str, edition: str = "mp",
311
- graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT) -> bool:
340
+ graphs_dir: Optional[str] = None, idle_timeout: float = _IDLE_TIMEOUT,
341
+ session: str = "default") -> bool:
312
342
  """Launch daemon as a detached background process. Returns True on success."""
313
- client = DaemonClient()
343
+ client = DaemonClient(session)
314
344
  if client.is_running():
315
345
  return True
316
346
 
317
347
  os.makedirs(_STATE_DIR, exist_ok=True)
318
- log_file = os.path.join(_STATE_DIR, "daemon.log")
348
+ log_file = os.path.join(_STATE_DIR, f"daemon-{session}.log")
319
349
 
320
350
  args = [
321
351
  sys.executable, "-m", "stata_cli.daemon",
322
352
  "--stata-path", stata_path,
323
353
  "--edition", edition,
324
354
  "--idle-timeout", str(int(idle_timeout)),
355
+ "--session", session,
325
356
  ]
326
357
  if graphs_dir:
327
358
  args += ["--graphs-dir", graphs_dir]
@@ -345,9 +376,9 @@ def start_daemon(stata_path: str, edition: str = "mp",
345
376
  return False
346
377
 
347
378
 
348
- def stop_daemon() -> bool:
379
+ def stop_daemon(session: str = "default") -> bool:
349
380
  """Ask the daemon to shut down gracefully."""
350
- client = DaemonClient()
381
+ client = DaemonClient(session)
351
382
  if not client.is_running():
352
383
  return True
353
384
  if client.connect():
@@ -362,7 +393,7 @@ def stop_daemon() -> bool:
362
393
  return True
363
394
  # Force kill
364
395
  try:
365
- with open(_PID_FILE) as fh:
396
+ with open(_pid_file(session)) as fh:
366
397
  pid = json.load(fh)["pid"]
367
398
  os.kill(pid, signal.SIGKILL if not _IS_WINDOWS else signal.SIGTERM)
368
399
  except Exception:
@@ -370,9 +401,43 @@ def stop_daemon() -> bool:
370
401
  return True
371
402
 
372
403
 
373
- def daemon_status() -> Optional[Dict[str, Any]]:
374
- """Query the running daemon's status. Returns None if not running."""
375
- client = DaemonClient()
404
+ def stop_all_daemons() -> int:
405
+ """Stop all running daemon sessions. Returns count stopped."""
406
+ import glob
407
+ count = 0
408
+ for pid_path in glob.glob(os.path.join(_STATE_DIR, "daemon-*.pid")):
409
+ name = os.path.basename(pid_path)
410
+ session = name[len("daemon-"):-len(".pid")]
411
+ stop_daemon(session)
412
+ count += 1
413
+ return count
414
+
415
+
416
+ def list_sessions() -> list:
417
+ """Return list of running session info dicts."""
418
+ import glob
419
+ sessions = []
420
+ for pid_path in glob.glob(os.path.join(_STATE_DIR, "daemon-*.pid")):
421
+ name = os.path.basename(pid_path)
422
+ session = name[len("daemon-"):-len(".pid")]
423
+ client = DaemonClient(session)
424
+ if not client.is_running():
425
+ continue
426
+ info = {"session": session}
427
+ if client.connect():
428
+ try:
429
+ status = client.send("status")
430
+ info.update(status)
431
+ except Exception:
432
+ pass
433
+ client.close()
434
+ sessions.append(info)
435
+ return sessions
436
+
437
+
438
+ def daemon_status(session: str = "default") -> Optional[Dict[str, Any]]:
439
+ """Query a running daemon's status. Returns None if not running."""
440
+ client = DaemonClient(session)
376
441
  if not client.is_running():
377
442
  return None
378
443
  if not client.connect():
@@ -394,5 +459,6 @@ if __name__ == "__main__":
394
459
  parser.add_argument("--edition", default="mp")
395
460
  parser.add_argument("--graphs-dir", default=None)
396
461
  parser.add_argument("--idle-timeout", type=float, default=_IDLE_TIMEOUT)
462
+ parser.add_argument("--session", default="default")
397
463
  args = parser.parse_args()
398
- run_daemon_server(args.stata_path, args.edition, args.graphs_dir, args.idle_timeout)
464
+ 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.1
4
4
  Summary: Command-line interface for running Stata commands via PyStata
5
5
  License: MIT
6
6
  Keywords: stata,cli,statistics,data-science
@@ -51,7 +51,7 @@ A command-line interface for [Stata](https://www.stata.com/) via PyStata — bui
51
51
  | **Frame Management** | List Stata frames and current working frame via `frame` |
52
52
  | **Help System** | Browse Stata help topics with SMCL-to-plain-text conversion |
53
53
  | **Graph Export** | Auto-detect and export graphs as PNG/SVG/PDF to `~/.stata-cli/graphs/` |
54
- | **Daemon Mode** | Persistent background process for sub-second execution via Unix socket |
54
+ | **Daemon Mode** | Persistent background process for sub-second execution; parallel sessions via `--session` |
55
55
  | **Output Control** | Compact mode, JSON output, token limit management, log file output |
56
56
  | **Interruption** | Send break signal to stop long-running commands |
57
57
  | **Skill Library** | Built-in Stata reference with 57 topics: syntax, econometrics, causal inference, packages |
@@ -290,13 +290,32 @@ stata-cli daemon stop # Shut down
290
290
  |---------|-------------|
291
291
  | `daemon start` | Start the background daemon process |
292
292
  | `daemon stop` | Graceful shutdown |
293
- | `daemon status` | Show PID, Stata path, uptime, idle time |
293
+ | `daemon stop --all` | Stop all running sessions |
294
+ | `daemon status` | Show all running sessions (PID, uptime, idle) |
294
295
  | `daemon restart` | Stop + start (clean Stata state) |
295
296
 
296
297
  Commands auto-route through the daemon when it is running. Use `--no-daemon` to force direct execution.
297
298
 
298
299
  The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle-timeout`).
299
300
 
301
+ ### Parallel Sessions
302
+
303
+ Run multiple independent Stata instances — like opening multiple Stata windows:
304
+
305
+ ```bash
306
+ # Start named sessions
307
+ stata-cli --session proj_a daemon start
308
+ stata-cli --session proj_b daemon start
309
+
310
+ # Each session has its own data, estimates, and macros
311
+ stata-cli --session proj_a run "use project_a.dta, clear"
312
+ stata-cli --session proj_b run "use project_b.dta, clear"
313
+
314
+ # Route any command to a specific session
315
+ stata-cli --session proj_a run "regress price mpg weight"
316
+ stata-cli --session proj_b return e
317
+ ```
318
+
300
319
  ## Advanced Usage
301
320
 
302
321
  ### Global Options
@@ -305,6 +324,7 @@ The daemon auto-shuts down after 1 hour of inactivity (configurable with `--idle
305
324
  |--------|-------------|---------|
306
325
  | `--stata-path PATH` | Stata installation directory | auto-detected |
307
326
  | `--edition [mp\|se\|be]` | Stata edition | `mp` |
327
+ | `--session NAME` | Daemon session name (for parallel sessions) | `default` |
308
328
  | `--compact` | Strip verbose output noise | off |
309
329
  | `--json` | Structured JSON output | off |
310
330
  | `--timeout SECONDS` | Execution timeout | 600 |
@@ -1 +0,0 @@
1
- __version__ = "0.4.2"
File without changes