aigroup-stata-mcp 1.0.3__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.
Files changed (38) hide show
  1. aigroup_stata_mcp-1.0.3.dist-info/METADATA +345 -0
  2. aigroup_stata_mcp-1.0.3.dist-info/RECORD +38 -0
  3. aigroup_stata_mcp-1.0.3.dist-info/WHEEL +4 -0
  4. aigroup_stata_mcp-1.0.3.dist-info/entry_points.txt +5 -0
  5. aigroup_stata_mcp-1.0.3.dist-info/licenses/LICENSE +21 -0
  6. stata_mcp/__init__.py +18 -0
  7. stata_mcp/cli/__init__.py +8 -0
  8. stata_mcp/cli/_cli.py +95 -0
  9. stata_mcp/core/__init__.py +14 -0
  10. stata_mcp/core/data_info/__init__.py +11 -0
  11. stata_mcp/core/data_info/_base.py +288 -0
  12. stata_mcp/core/data_info/csv.py +123 -0
  13. stata_mcp/core/data_info/dta.py +70 -0
  14. stata_mcp/core/stata/__init__.py +13 -0
  15. stata_mcp/core/stata/stata_controller/__init__.py +9 -0
  16. stata_mcp/core/stata/stata_controller/controller.py +208 -0
  17. stata_mcp/core/stata/stata_do/__init__.py +9 -0
  18. stata_mcp/core/stata/stata_do/do.py +177 -0
  19. stata_mcp/core/stata/stata_finder/__init__.py +9 -0
  20. stata_mcp/core/stata/stata_finder/base.py +294 -0
  21. stata_mcp/core/stata/stata_finder/finder.py +193 -0
  22. stata_mcp/core/stata/stata_finder/linux.py +43 -0
  23. stata_mcp/core/stata/stata_finder/macos.py +88 -0
  24. stata_mcp/core/stata/stata_finder/windows.py +191 -0
  25. stata_mcp/server/__init__.py +8 -0
  26. stata_mcp/server/main.py +153 -0
  27. stata_mcp/server/prompts/__init__.py +8 -0
  28. stata_mcp/server/prompts/core_prompts.py +122 -0
  29. stata_mcp/server/tools/__init__.py +10 -0
  30. stata_mcp/server/tools/core_tools.py +59 -0
  31. stata_mcp/server/tools/file_tools.py +163 -0
  32. stata_mcp/server/tools/stata_tools.py +221 -0
  33. stata_mcp/utils/Installer/__init__.py +7 -0
  34. stata_mcp/utils/Installer/installer.py +85 -0
  35. stata_mcp/utils/Prompt/__init__.py +74 -0
  36. stata_mcp/utils/Prompt/string.py +91 -0
  37. stata_mcp/utils/__init__.py +23 -0
  38. stata_mcp/utils/usable.py +244 -0
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import re
6
+ import time
7
+
8
+ import pexpect
9
+
10
+
11
+ class StataController:
12
+
13
+ """Stata控制器类,用于管理Stata会话和执行命令"""
14
+ def __init__(self, stata_cli: str = None, timeout: int = 30):
15
+ """
16
+ Initialize the Stata controller.
17
+
18
+ Args:
19
+ stata_cli (str): Path to the Stata command-line executable.
20
+ timeout (int): Timeout for command execution (in seconds).
21
+ """
22
+ self.stata_cli_path = stata_cli
23
+ self.child = None
24
+ self.timeout = timeout
25
+ self.start()
26
+
27
+ @property
28
+ def STATA_CLI(self):
29
+ """获取Stata CLI路径"""
30
+ return self.stata_cli_path
31
+
32
+ def _expect_prompt(self, timeout=None):
33
+ """
34
+ Wait for the Stata prompt, indicating command completion.
35
+
36
+ Args:
37
+ timeout (int, optional): Timeout for this wait; if not provided, use default.
38
+
39
+ Returns:
40
+ int: The index returned by pexpect.expect.
41
+ """
42
+ if timeout is None:
43
+ timeout = self.timeout
44
+
45
+ # Use a set of patterns to match various prompt scenarios
46
+ patterns = [
47
+ r"\r\n\. ", # Standard prompt
48
+ r"\r\n: ", # Continuation prompt
49
+ r"\r\n--more--", # More content prompt
50
+ r"r\(\d+\);", # Error prompt
51
+ pexpect.TIMEOUT, # Timeout
52
+ pexpect.EOF, # End of program
53
+ ]
54
+
55
+ index = self.child.expect(patterns, timeout=timeout)
56
+
57
+ # Handle matched patterns
58
+ if index == 2: # --more-- prompt; send space to continue
59
+ self.child.send(" ")
60
+ return self._expect_prompt(timeout) # Recurse until actual prompt
61
+ elif index == 3: # Error prompt
62
+ # Continue waiting until standard prompt appears
63
+ try:
64
+ self.child.expect(r"\r\n\. ", timeout=5)
65
+ except pexpect.TIMEOUT:
66
+ pass # Ignore timeout and return error index
67
+ return index
68
+ elif index == 4: # Timeout
69
+ # Try sending a newline to trigger the prompt
70
+ self.child.sendline("")
71
+ try:
72
+ return self.child.expect(
73
+ [r"\r\n\. ", pexpect.TIMEOUT], timeout=5)
74
+ except pexpect.TIMEOUT:
75
+ return index
76
+
77
+ return index
78
+
79
+ def run(self, command, timeout=None):
80
+ """
81
+ Execute a Stata command and wait for completion.
82
+
83
+ Args:
84
+ command (str): The Stata command to execute.
85
+ timeout (int, optional): Timeout for this command.
86
+
87
+ Returns:
88
+ str: The output of the command execution.
89
+
90
+ Raises:
91
+ RuntimeError: If the command times out or other errors occur.
92
+ """
93
+ if timeout is None:
94
+ timeout = self.timeout
95
+
96
+ # Send the command
97
+ self.child.sendline(command)
98
+
99
+ # Wait for the command to complete
100
+ result = self._expect_prompt(timeout)
101
+
102
+ # Capture the output
103
+ output = self.child.before.strip()
104
+
105
+ # Check for errors
106
+ if result == 3: # Error prompt index
107
+ error_match = re.search(r"r\((\d+)\);", output)
108
+ if error_match:
109
+ error_code = error_match.group(1)
110
+ raise RuntimeError(f"Stata error r({error_code}): {output}")
111
+ elif result == 4: # Timeout
112
+ raise RuntimeError(f"Command timed out (> {timeout}s): {command}")
113
+ elif result == 5: # EOF
114
+ raise RuntimeError(
115
+ f"Stata session terminated unexpectedly: {output}")
116
+
117
+ return output
118
+
119
+ def run_with_retry(self, command, max_retries=3, timeout=None):
120
+ """
121
+ Execute a command with a retry mechanism.
122
+
123
+ Args:
124
+ command (str): The Stata command to execute.
125
+ max_retries (int): Maximum number of retry attempts.
126
+ timeout (int, optional): Timeout for this command.
127
+
128
+ Returns:
129
+ str: The output of the command execution.
130
+
131
+ Raises:
132
+ RuntimeError: If all retry attempts fail.
133
+ """
134
+ retries = 0
135
+ last_error = None
136
+
137
+ while retries < max_retries:
138
+ try:
139
+ return self.run(command, timeout)
140
+ except RuntimeError as e:
141
+ last_error = e
142
+ retries += 1
143
+ # If it's a timeout error and we can retry, restart the session
144
+ if "timed out" in str(e) and retries < max_retries:
145
+ self.restart()
146
+ time.sleep(1) # Brief pause before retry
147
+
148
+ # All retries failed
149
+ raise RuntimeError(
150
+ f"Command failed after {max_retries} attempts: {last_error}")
151
+
152
+ def start(self):
153
+ """
154
+ Start the Stata session.
155
+ """
156
+ self.child = pexpect.spawn(
157
+ self.STATA_CLI, encoding="utf-8", timeout=self.timeout
158
+ )
159
+ self._expect_prompt()
160
+
161
+ def restart(self):
162
+ """
163
+ Restart the Stata session.
164
+ """
165
+ self.close()
166
+ self.start()
167
+
168
+ def close(self):
169
+ """
170
+ Close the Stata session.
171
+ """
172
+ if self.child and not self.child.closed:
173
+ try:
174
+ self.child.sendline("exit, clear")
175
+ self.child.expect(pexpect.EOF, timeout=5)
176
+ except Exception as e:
177
+ print(
178
+ f"Warning: Could not close Stata session with error: {e}")
179
+ finally:
180
+ self.child.close()
181
+
182
+
183
+ if __name__ == "__main__":
184
+ url = "https://pub-b55c5837ee41480ba0f902096dd9725d.r2.dev/01_OLS.dta"
185
+ stata_cli = "stata-mp"
186
+ var_list = [] # e.g., ["weight", "height"]
187
+ var_str = " ".join(var_list) if var_list else ""
188
+
189
+ # Use a longer timeout for the session
190
+ temp_stata_session = StataController(stata_cli=stata_cli, timeout=60)
191
+
192
+ try:
193
+ # Execute command with retry
194
+ use_data = temp_stata_session.run_with_retry(f"use {url}, clear")
195
+ if "not found" in use_data or "server reported server error" in use_data:
196
+ print("Stata data not found. Please check the path.")
197
+ else:
198
+ # For commands that may require more time, specify a longer timeout
199
+ summarize = temp_stata_session.run(
200
+ f"summarize {var_str}", timeout=120)
201
+ describe = temp_stata_session.run(f"describe {var_str}")
202
+ result = {"summarize": summarize, "describe": describe}
203
+ print(result.get("summarize"))
204
+ except Exception as e:
205
+ print(f"Error: {e}")
206
+ finally:
207
+ # Ensure the session is properly closed
208
+ temp_stata_session.close()
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ from .do import StataDo
6
+
7
+ __all__ = [
8
+ "StataDo"
9
+ ]
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import logging
6
+ import os
7
+ import subprocess
8
+
9
+ from ....utils import get_nowtime
10
+
11
+
12
+ class StataDo:
13
+
14
+ """Stata do文件执行器,用于执行Stata脚本并管理日志"""
15
+ def __init__(self,
16
+ stata_cli: str,
17
+ log_file_path: str,
18
+ dofile_base_path: str,
19
+ sys_os: str = None):
20
+ """
21
+ Initialize Stata executor
22
+
23
+ Args:
24
+ stata_cli: Path to Stata command line tool
25
+ log_file_path: Path for storing log files
26
+ dofile_base_path: Base path for do files
27
+ sys_os: Operating system type
28
+ """
29
+ self.stata_cli = stata_cli
30
+ self.log_file_path = log_file_path
31
+ self.dofile_base_path = dofile_base_path
32
+ if sys_os:
33
+ self.sys_os = sys_os
34
+ else:
35
+ from ....utils import get_os
36
+ self.sys_os = get_os()
37
+
38
+ def set_cli(self, cli_path):
39
+ """设置Stata CLI路径"""
40
+ self.stata_cli = cli_path
41
+
42
+ @property
43
+ def STATA_CLI(self):
44
+ """获取Stata CLI路径"""
45
+ return self.stata_cli
46
+
47
+ def execute_dofile(self,
48
+ dofile_path: str,
49
+ log_file_name: str = None,
50
+ is_replace: bool = True) -> str:
51
+ """
52
+ Execute Stata do file and return log file path
53
+
54
+ Args:
55
+ dofile_path (str): Path to do file
56
+ log_file_name (str, optional): File name of log
57
+ is_replace (bool): Whether replace the log file if exists before. Default is True
58
+
59
+ Returns:
60
+ str: Path to generated log file
61
+
62
+ Raises:
63
+ ValueError: Unsupported operating system
64
+ RuntimeError: Stata execution error
65
+ """
66
+ nowtime = get_nowtime()
67
+ log_name = log_file_name or nowtime
68
+ log_file = os.path.join(self.log_file_path, f"{log_name}.log")
69
+
70
+ if self.sys_os == "Darwin" or self.sys_os == "Linux":
71
+ self._execute_unix_like(dofile_path, log_file, is_replace)
72
+ elif self.sys_os == "Windows":
73
+ self._execute_windows(dofile_path, log_file, nowtime, is_replace)
74
+ else:
75
+ raise ValueError(f"Unsupported operating system: {self.sys_os}")
76
+
77
+ return log_file
78
+
79
+ def _execute_unix_like(self, dofile_path: str, log_file: str, is_replace: bool = True):
80
+ """
81
+ Execute Stata on macOS/Linux systems
82
+
83
+ Args:
84
+ dofile_path: Path to do file
85
+ log_file: Path to log file
86
+ is_replace: Whether replace the log file if exists.
87
+
88
+ Raises:
89
+ RuntimeError: Stata execution error
90
+ """
91
+ proc = subprocess.Popen(
92
+ [self.STATA_CLI], # Launch the Stata CLI
93
+ stdin=subprocess.PIPE, # Prepare to send commands
94
+ stdout=subprocess.PIPE,
95
+ stderr=subprocess.PIPE,
96
+ text=True,
97
+ shell=True, # Required when the path contains spaces
98
+ )
99
+
100
+ # Execute commands sequentially in Stata
101
+ replace_clause = ", replace" if is_replace else ""
102
+
103
+ commands = f"""
104
+ log using "{log_file}"{replace_clause}
105
+ do "{dofile_path}"
106
+ log close
107
+ exit, STATA
108
+ """
109
+ stdout, stderr = proc.communicate(
110
+ input=commands
111
+ ) # Send commands and wait for completion
112
+
113
+ if proc.returncode != 0:
114
+ logging.error(f"Stata execution failed: {stderr}")
115
+ raise RuntimeError(f"Something went wrong: {stderr}")
116
+ else:
117
+ logging.info(
118
+ f"Stata execution completed successfully. Log file: {log_file}")
119
+
120
+ def _execute_windows(self, dofile_path: str, log_file: str, nowtime: str, is_replace: bool = True):
121
+ """
122
+ Execute Stata on Windows systems
123
+
124
+ Args:
125
+ dofile_path: Path to do file
126
+ log_file: Path to log file
127
+ nowtime: Timestamp for generating temporary file names
128
+ """
129
+ # Windows approach - use the /e /q flags for clean batch processing
130
+ # Create a temporary batch file
131
+ batch_file = os.path.join(self.dofile_base_path, f"{nowtime}_batch.do")
132
+
133
+ replace_clause = ", replace" if is_replace else ""
134
+ try:
135
+ with open(batch_file, "w", encoding="utf-8") as f:
136
+ f.write(f'log using "{log_file}"{replace_clause}\n')
137
+ f.write(f'do "{dofile_path}"\n')
138
+ f.write("log close\n")
139
+ f.write("exit, STATA\n")
140
+
141
+ # Run Stata on Windows using /e /q for clean batch processing
142
+ # /e: batch mode (execute and exit)
143
+ # /q: quiet mode (no startup messages)
144
+ cmd = f'"{self.STATA_CLI}" /e /q do "{batch_file}"'
145
+ result = subprocess.run(
146
+ cmd, shell=True, capture_output=True, text=True)
147
+
148
+ if result.returncode != 0:
149
+ logging.error(
150
+ f"Stata execution failed on Windows: {result.stderr}")
151
+ raise RuntimeError(
152
+ f"Windows Stata execution failed: {result.stderr}")
153
+ else:
154
+ logging.info(
155
+ f"Stata execution completed successfully on Windows. Log file: {log_file}")
156
+
157
+ except Exception as e:
158
+ logging.error(f"Error during Windows Stata execution: {str(e)}")
159
+ raise
160
+ finally:
161
+ # Clean up temporary batch file
162
+ if os.path.exists(batch_file):
163
+ try:
164
+ os.remove(batch_file)
165
+ logging.debug(
166
+ f"Temporary batch file removed: {batch_file}")
167
+ except Exception as e:
168
+ logging.warning(
169
+ f"Failed to remove temporary batch file "
170
+ f"{batch_file}: {str(e)}")
171
+
172
+ @staticmethod
173
+ def read_log(log_file_path, mode="r", encoding="utf-8") -> str:
174
+ """读取Stata日志文件内容"""
175
+ with open(log_file_path, mode, encoding=encoding) as file:
176
+ log_content = file.read()
177
+ return log_content
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ from .finder import StataFinder
6
+
7
+ __all__ = [
8
+ "StataFinder",
9
+ ]
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+
5
+ import os
6
+ import re
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Dict, Iterable, List, Optional, Union
11
+
12
+
13
+ @dataclass
14
+ class StataEditionConfig:
15
+
16
+
17
+ """
18
+ StataEditionConfig class for comparing Stata versions with sorting support.
19
+
20
+ Attributes:
21
+ edition (str): Edition type (mp > se > be > ic > default)
22
+ version (Union[int, float]): Version number (e.g., 18, 19.5). Default 99 if not found, indicating current default version
23
+ path (str): Full path to Stata executable
24
+
25
+ Comparison Rules:
26
+ 1. First compare edition priority: mp > se > be > ic > default (edition type always has priority)
27
+ 2. Then compare numeric version: higher > lower (only for same edition type)
28
+ 3. Support float versions like 19.5 > 19
29
+ 4. Version 99 is used when version info is not available (default edition gets highest version within its type)
30
+
31
+ Example:
32
+ >>> p1 = StataEditionConfig("mp", 18, "/usr/local/bin/stata-mp")
33
+ >>> p2 = StataEditionConfig.from_path("/usr/local/bin/stata") # Auto-detect as default with version 99
34
+ >>> p1 > p2 # True (mp edition has higher priority than default, regardless of version numbers)
35
+ """
36
+
37
+ edition: str
38
+ version: Union[int, float]
39
+ path: str
40
+
41
+ # Edition priority mapping
42
+ _EDITION_PRIORITY = {
43
+ "mp": 5,
44
+ "se": 4,
45
+ "be": 3,
46
+ "ic": 2,
47
+ "default": 1,
48
+ "unknown": 0,
49
+ }
50
+
51
+ def __post_init__(self):
52
+ """Validation and processing after initialization."""
53
+ # Normalize edition type to lowercase
54
+ self.edition = self.edition.lower()
55
+
56
+ # If edition type is not in priority mapping, mark as unknown
57
+ if self.edition not in self._EDITION_PRIORITY:
58
+ self.edition = "unknown"
59
+
60
+ @classmethod
61
+ def from_path(cls, path: str) -> 'StataEditionConfig':
62
+ """
63
+ Create StataEditionConfig from path, automatically extracting edition and version.
64
+
65
+ Args:
66
+ path: Full path to Stata executable
67
+
68
+ Returns:
69
+ StataEditionConfig with auto-detected edition and version
70
+ """
71
+ import os
72
+
73
+ filename = os.path.basename(path).lower()
74
+ full_path_lower = path.lower()
75
+
76
+ # Extract edition
77
+ edition = "default"
78
+ edition_patterns = [
79
+ (r'stata-mp', 'mp'),
80
+ (r'satamp', 'mp'),
81
+ (r'stata-se', 'se'),
82
+ (r'statase', 'se'),
83
+ (r'sata-be', 'be'),
84
+ (r'satabe', 'be'),
85
+ (r'sata-ic', 'ic'),
86
+ (r'sataic', 'ic'),
87
+ ]
88
+
89
+ for pattern, ed in edition_patterns:
90
+ if re.search(pattern, filename):
91
+ edition = ed
92
+ break
93
+
94
+ # Extract version (default to 99 if not found, indicating current default version)
95
+ version = 99
96
+
97
+ # Try to extract version from directory name first
98
+ dir_version_match = re.search(r'stata(\d+(?:\.\d+)?)', full_path_lower)
99
+ if dir_version_match:
100
+ try:
101
+ version = float(dir_version_match.group(1))
102
+ except ValueError:
103
+ pass
104
+
105
+ # Try to extract version from filename
106
+ file_version_patterns = [
107
+ r'stata-[a-z]+-(\d+(?:\.\d+)?)', # stata-mp-17.5
108
+ r'stata(\d+(?:\.\d+)?)', # stata17.5
109
+ ]
110
+
111
+ for pattern in file_version_patterns:
112
+ file_version_match = re.search(pattern, filename)
113
+ if file_version_match:
114
+ try:
115
+ file_version = float(file_version_match.group(1))
116
+ # Only use reasonable version numbers (1-30)
117
+ if 1 <= file_version <= 30:
118
+ version = max(version, file_version)
119
+ break
120
+ except ValueError:
121
+ continue
122
+
123
+ return cls(edition=edition, version=version, path=path)
124
+
125
+ @property
126
+ def edition_priority(self) -> int:
127
+ """Get the priority value of the edition type."""
128
+ return self._EDITION_PRIORITY[self.edition]
129
+
130
+ def __lt__(self, other) -> bool:
131
+ """Less than comparison for sorting."""
132
+ if not isinstance(other, StataEditionConfig):
133
+ return NotImplemented
134
+
135
+ # First compare edition priority
136
+ if self.edition_priority != other.edition_priority:
137
+ return self.edition_priority < other.edition_priority
138
+
139
+ # Same edition, compare version number
140
+ return self.version < other.version
141
+
142
+ def __le__(self, other) -> bool:
143
+ """Less than or equal comparison."""
144
+ return self < other or self == other
145
+
146
+ def __gt__(self, other) -> bool:
147
+ """Greater than comparison."""
148
+ if not isinstance(other, StataEditionConfig):
149
+ return NotImplemented
150
+
151
+ # First compare edition priority
152
+ if self.edition_priority != other.edition_priority:
153
+ return self.edition_priority > other.edition_priority
154
+
155
+ # Same edition, compare version number
156
+ return self.version > other.version
157
+
158
+ def __ge__(self, other) -> bool:
159
+ """Greater than or equal comparison."""
160
+ return self > other or self == other
161
+
162
+ def __eq__(self, other) -> bool:
163
+ """Equality comparison."""
164
+ if not isinstance(other, StataEditionConfig):
165
+ return NotImplemented
166
+
167
+ return (self.edition_priority == other.edition_priority and
168
+ self.version == other.version)
169
+
170
+ def __str__(self) -> str:
171
+ """String representation - returns the path."""
172
+ return self.path
173
+
174
+ def __repr__(self) -> str:
175
+ """Detailed string representation - returns the path."""
176
+ return self.path
177
+
178
+ def __int__(self) -> int:
179
+ """Integer conversion - returns the version number."""
180
+ return int(self.version)
181
+
182
+ def __float__(self) -> float:
183
+ """Float conversion - returns the version number."""
184
+ return float(self.version)
185
+
186
+ @property
187
+ def stata_cli_path(self) -> str:
188
+ """Get the Stata CLI path."""
189
+ return self.path
190
+
191
+
192
+ class FinderBase(ABC):
193
+ stata_cli: str = None
194
+
195
+ def __init__(self, stata_cli: str = None):
196
+ # If there is any setting, use the input and environment first
197
+ self.stata_cli = stata_cli or os.getenv("STATA_CLI") or os.getenv("stata_cli")
198
+
199
+ def find_stata(self) -> str | None:
200
+ if self.stata_cli:
201
+ return self.stata_cli
202
+ return self.finder()
203
+
204
+ @abstractmethod
205
+ def finder(self) -> str:
206
+ """
207
+ Find the Stata executable on the current platform.
208
+
209
+ This method must be implemented by each platform-specific finder class
210
+ to locate Stata installations using platform-appropriate search strategies.
211
+
212
+ Returns:
213
+ str: The full path to the Stata executable
214
+
215
+ Raises:
216
+ FileNotFoundError: If no Stata installation is found
217
+
218
+ Note:
219
+ This is an abstract method and must be implemented by concrete finder classes
220
+ such as FinderMacOS, FinderWindows, or FinderLinux.
221
+
222
+ Each platform should implement appropriate search strategies for finding
223
+ Stata installations in their typical locations (e.g., /Applications for macOS,
224
+ Program Files for Windows, system PATH for Linux).
225
+ """
226
+ ...
227
+
228
+ @abstractmethod
229
+ def find_path_base(self) -> Dict[str, List[str]]:
230
+ """简要描述函数功能"""
231
+ ...
232
+
233
+ @staticmethod
234
+ def priority() -> Dict[str, List[str]]:
235
+ """简要描述函数功能"""
236
+ name_priority = {
237
+ "mp": ["stata-mp"],
238
+ "se": ["stata-se"],
239
+ "be": ["stata-be"],
240
+ "default": ["stata"],
241
+ }
242
+ return name_priority
243
+
244
+ @staticmethod
245
+ def _is_executable(p: Path) -> bool:
246
+ """简要描述函数功能"""
247
+ try:
248
+ return p.is_file() and os.access(p, os.X_OK)
249
+ except OSError:
250
+ return False
251
+
252
+ def find_from_bin(self,
253
+ *,
254
+ priority: Optional[Iterable[str]] = None) -> List[StataEditionConfig]:
255
+ """
256
+ Find all available Stata executables in bin directories.
257
+
258
+ Args:
259
+ priority: Edition priority order (default: ["mp", "se", "be", "default"])
260
+
261
+ Returns:
262
+ List of all executable Stata paths found in bin directories,
263
+ ordered by priority. Returns empty list if no executables found.
264
+ """
265
+ pr = list(priority) if priority else ["mp", "se", "be", "default"]
266
+ name_priority = self.priority()
267
+ bins = self.find_path_base().get("bin")
268
+
269
+ if not bins:
270
+ return []
271
+
272
+ # Build ordered list of executable names by priority
273
+ ordered_names: List[str] = []
274
+ for key in pr:
275
+ ordered_names.extend(name_priority.get(key, []))
276
+
277
+ found_executables: List[StataEditionConfig] = []
278
+
279
+ # Search for executables in all bin directories
280
+ for b in bins:
281
+ base = Path(b)
282
+
283
+ # Check weather the bin directory exists
284
+ if not base.exists():
285
+ continue
286
+
287
+ for name in ordered_names:
288
+ p = base / name
289
+ if self._is_executable(p):
290
+ # Convert path to StataEditionConfig
291
+ config = StataEditionConfig.from_path(str(p))
292
+ found_executables.append(config)
293
+
294
+ return found_executables