mint-sdk 1.0.0a1__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 (65) hide show
  1. mint_sdk/__init__.py +131 -0
  2. mint_sdk/_discover.py +159 -0
  3. mint_sdk/_prompt.py +228 -0
  4. mint_sdk/_version.py +24 -0
  5. mint_sdk/ai_instructions.md.template +169 -0
  6. mint_sdk/app.py +264 -0
  7. mint_sdk/cli.py +596 -0
  8. mint_sdk/cli_commands/__init__.py +0 -0
  9. mint_sdk/cli_commands/_utils.py +22 -0
  10. mint_sdk/cli_commands/auth_cmd.py +112 -0
  11. mint_sdk/cli_commands/experiment_cmd.py +198 -0
  12. mint_sdk/cli_commands/project_cmd.py +95 -0
  13. mint_sdk/cli_commands/status_cmd.py +68 -0
  14. mint_sdk/client/__init__.py +35 -0
  15. mint_sdk/client/_config.py +118 -0
  16. mint_sdk/client/_exceptions.py +130 -0
  17. mint_sdk/client/_http.py +119 -0
  18. mint_sdk/client/_types.py +90 -0
  19. mint_sdk/client/client.py +125 -0
  20. mint_sdk/client/resources/__init__.py +0 -0
  21. mint_sdk/client/resources/auth.py +75 -0
  22. mint_sdk/client/resources/experiments.py +277 -0
  23. mint_sdk/client/resources/plugins.py +18 -0
  24. mint_sdk/client/resources/projects.py +82 -0
  25. mint_sdk/context.py +103 -0
  26. mint_sdk/deps_command.py +271 -0
  27. mint_sdk/dev_command.py +461 -0
  28. mint_sdk/docs/__init__.py +1 -0
  29. mint_sdk/docs/bundled/frontend.json +6845 -0
  30. mint_sdk/docs/cache.py +126 -0
  31. mint_sdk/docs/formatter.py +709 -0
  32. mint_sdk/docs/frontend_extractor.py +79 -0
  33. mint_sdk/docs/python_extractor.py +442 -0
  34. mint_sdk/docs/search.py +137 -0
  35. mint_sdk/docs_command.py +293 -0
  36. mint_sdk/doctor_command.py +276 -0
  37. mint_sdk/exceptions.py +311 -0
  38. mint_sdk/export.py +348 -0
  39. mint_sdk/info_command.py +74 -0
  40. mint_sdk/init_command.py +601 -0
  41. mint_sdk/init_templates.py +968 -0
  42. mint_sdk/link_command.py +218 -0
  43. mint_sdk/local_database.py +163 -0
  44. mint_sdk/logging.py +49 -0
  45. mint_sdk/logs_command.py +137 -0
  46. mint_sdk/migrations/__init__.py +22 -0
  47. mint_sdk/migrations/base.py +54 -0
  48. mint_sdk/migrations/errors.py +54 -0
  49. mint_sdk/migrations/locking.py +64 -0
  50. mint_sdk/migrations/ops.py +302 -0
  51. mint_sdk/migrations/runner.py +313 -0
  52. mint_sdk/models.py +51 -0
  53. mint_sdk/plugin.py +610 -0
  54. mint_sdk/py.typed +0 -0
  55. mint_sdk/remote_context.py +323 -0
  56. mint_sdk/repositories.py +277 -0
  57. mint_sdk/testing/__init__.py +36 -0
  58. mint_sdk/testing/plugins.py +135 -0
  59. mint_sdk/testing/recording_context.py +185 -0
  60. mint_sdk/testing/subprocess.py +88 -0
  61. mint_sdk/update_command.py +260 -0
  62. mint_sdk-1.0.0a1.dist-info/METADATA +155 -0
  63. mint_sdk-1.0.0a1.dist-info/RECORD +65 -0
  64. mint_sdk-1.0.0a1.dist-info/WHEEL +4 -0
  65. mint_sdk-1.0.0a1.dist-info/entry_points.txt +2 -0
mint_sdk/__init__.py ADDED
@@ -0,0 +1,131 @@
1
+ """
2
+ MINT Plugin SDK
3
+
4
+ SDK for building analysis plugins that integrate with the MINT platform.
5
+ """
6
+
7
+ from mint_sdk.context import PlatformContext
8
+
9
+ # Exceptions
10
+ from mint_sdk.exceptions import (
11
+ ConfigurationException,
12
+ ConflictException,
13
+ NotFoundException,
14
+ PermissionException,
15
+ PluginException,
16
+ PluginLifecycleException,
17
+ RepositoryException,
18
+ ValidationException,
19
+ )
20
+ from mint_sdk.export import auto_json_to_csv, auto_json_to_summary, auto_json_to_tree
21
+
22
+ # Local database (optional dependency)
23
+ from mint_sdk.local_database import LocalDatabase, LocalDatabaseConfig
24
+ from mint_sdk.logging import get_plugin_logger
25
+
26
+ # Migration framework (requires sqlalchemy — optional dependency)
27
+ try:
28
+ from mint_sdk.migrations import (
29
+ MigrationOps,
30
+ MigrationResult,
31
+ MigrationRunner,
32
+ PluginMigration,
33
+ )
34
+ except ImportError:
35
+ pass
36
+ from mint_sdk.models import PluginCapabilities, PluginMetadata, PluginType
37
+ from mint_sdk.plugin import (
38
+ AnalysisPlugin,
39
+ HealthStatus,
40
+ LifecycleHookResult,
41
+ PluginHealth,
42
+ )
43
+
44
+ # Repository protocols and data models
45
+ from mint_sdk.repositories import (
46
+ DesignData,
47
+ # Data models
48
+ Experiment,
49
+ # Repository protocols
50
+ ExperimentRepository,
51
+ PlatformConfig,
52
+ PluginAnalysisResult,
53
+ PluginDataRepository,
54
+ PluginExperimentData, # backward compatibility alias for DesignData
55
+ PluginRoleRepository,
56
+ User,
57
+ UserPluginRole,
58
+ UserRepository,
59
+ )
60
+
61
+ # App factory and helpers
62
+ from mint_sdk.app import (
63
+ PluginDependency,
64
+ SPAStaticFiles,
65
+ create_standalone_app,
66
+ require_context,
67
+ )
68
+
69
+ # API Client
70
+ from mint_sdk.client import MINTClient
71
+
72
+ try:
73
+ from mint_sdk._version import __version__
74
+ except ImportError:
75
+ __version__ = "0.0.0"
76
+
77
+ __all__ = [
78
+ # API Client
79
+ "MINTClient",
80
+ # Core plugin classes
81
+ "AnalysisPlugin",
82
+ "PluginMetadata",
83
+ "PluginCapabilities",
84
+ "PluginType",
85
+ "PlatformContext",
86
+ # Lifecycle types
87
+ "HealthStatus",
88
+ "PluginHealth",
89
+ "LifecycleHookResult",
90
+ # Exceptions
91
+ "PluginException",
92
+ "ValidationException",
93
+ "PermissionException",
94
+ "ConfigurationException",
95
+ "RepositoryException",
96
+ "NotFoundException",
97
+ "ConflictException",
98
+ "PluginLifecycleException",
99
+ # Local database
100
+ "LocalDatabase",
101
+ "LocalDatabaseConfig",
102
+ # Data models
103
+ "Experiment",
104
+ "DesignData",
105
+ "PluginExperimentData", # backward compatibility alias
106
+ "PluginAnalysisResult",
107
+ "User",
108
+ "UserPluginRole",
109
+ # Repository protocols
110
+ "ExperimentRepository",
111
+ "PluginDataRepository",
112
+ "PluginRoleRepository",
113
+ "UserRepository",
114
+ "PlatformConfig",
115
+ # Export utilities
116
+ "auto_json_to_tree",
117
+ "auto_json_to_csv",
118
+ "auto_json_to_summary",
119
+ # Logging
120
+ "get_plugin_logger",
121
+ # Migration framework
122
+ "MigrationOps",
123
+ "MigrationResult",
124
+ "MigrationRunner",
125
+ "PluginMigration",
126
+ # App factory and helpers
127
+ "create_standalone_app",
128
+ "SPAStaticFiles",
129
+ "PluginDependency",
130
+ "require_context",
131
+ ]
mint_sdk/_discover.py ADDED
@@ -0,0 +1,159 @@
1
+ """Plugin discovery from pyproject.toml without importing the plugin.
2
+
3
+ Parses entry points, module paths, and routes prefix by inspecting files
4
+ statically — no plugin code is executed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ import sys
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ try:
15
+ import tomllib
16
+ except ModuleNotFoundError: # Python < 3.11
17
+ import tomli as tomllib # type: ignore[no-redef]
18
+
19
+
20
+ @dataclass(slots=True)
21
+ class DiscoveredPlugin:
22
+ """Statically discovered plugin metadata."""
23
+
24
+ entry_point_name: str # "drp-analysis"
25
+ module_path: str # "mld_plugin_drp.plugin"
26
+ class_name: str # "DRPAnalysisPlugin"
27
+ factory_target: str # "mld_plugin_drp.plugin:create_standalone_app"
28
+ routes_prefix: str # "/drp" (from source) or "/drp-analysis" (fallback)
29
+ project_name: str # "mld-plugin-drp"
30
+ project_version: str | None # "0.2.0" or None if dynamic
31
+ project_description: str # from [project].description
32
+ has_frontend: bool # frontend/package.json exists
33
+
34
+
35
+ def _read_full_pyproject(project_dir: Path) -> dict:
36
+ """Read and return the full pyproject.toml as a dict."""
37
+ path = project_dir / "pyproject.toml"
38
+ if not path.exists():
39
+ print(f"Error: {path} not found.", file=sys.stderr)
40
+ sys.exit(1)
41
+ with open(path, "rb") as f:
42
+ return tomllib.load(f)
43
+
44
+
45
+ def _parse_entry_point(data: dict) -> tuple[str, str, str] | None:
46
+ """Extract (name, module_path, class_name) from [project.entry-points."mld.plugins"].
47
+
48
+ Returns None if no mld.plugins entry point is found.
49
+ """
50
+ entry_points = (
51
+ data.get("project", {}).get("entry-points", {}).get("mld.plugins", {})
52
+ )
53
+ if not entry_points:
54
+ return None
55
+
56
+ # Take the first entry point
57
+ name, value = next(iter(entry_points.items()))
58
+ # value is "mld_plugin_drp.plugin:DRPAnalysisPlugin"
59
+ if ":" not in value:
60
+ return None
61
+ module_path, class_name = value.rsplit(":", 1)
62
+ return name, module_path, class_name
63
+
64
+
65
+ def _parse_routes_prefix_from_source(
66
+ module_path: str, project_dir: Path
67
+ ) -> str | None:
68
+ """Parse PLUGIN_ROUTES_PREFIX from the plugin source file without importing it.
69
+
70
+ Looks for patterns like:
71
+ PLUGIN_ROUTES_PREFIX = "/drp"
72
+ PLUGIN_ROUTES_PREFIX = '/drp'
73
+ """
74
+ # Convert module path to file path: mld_plugin_drp.plugin -> src/mld_plugin_drp/plugin.py
75
+ parts = module_path.split(".")
76
+ candidates = [
77
+ project_dir / "src" / Path(*parts).with_suffix(".py"),
78
+ project_dir / Path(*parts).with_suffix(".py"),
79
+ ]
80
+
81
+ for source_path in candidates:
82
+ if not source_path.exists():
83
+ continue
84
+ try:
85
+ content = source_path.read_text()
86
+ except OSError:
87
+ continue
88
+
89
+ match = re.search(
90
+ r'PLUGIN_ROUTES_PREFIX\s*=\s*["\'](/[^"\']+)["\']', content
91
+ )
92
+ if match:
93
+ return match.group(1)
94
+
95
+ return None
96
+
97
+
98
+ def discover_plugin(project_dir: Path) -> DiscoveredPlugin:
99
+ """Discover plugin metadata from pyproject.toml and source files.
100
+
101
+ Raises SystemExit if the project directory lacks required configuration.
102
+ """
103
+ data = _read_full_pyproject(project_dir)
104
+
105
+ project = data.get("project")
106
+ if not project:
107
+ print("Error: pyproject.toml has no [project] table.", file=sys.stderr)
108
+ sys.exit(1)
109
+
110
+ entry_point = _parse_entry_point(data)
111
+ if not entry_point:
112
+ print(
113
+ "Error: No [project.entry-points.\"mld.plugins\"] found in pyproject.toml.",
114
+ file=sys.stderr,
115
+ )
116
+ sys.exit(1)
117
+
118
+ ep_name, module_path, class_name = entry_point
119
+
120
+ # Routes prefix: parse from source, fallback to /{entry_point_name}
121
+ routes_prefix = _parse_routes_prefix_from_source(module_path, project_dir)
122
+ if routes_prefix is None:
123
+ routes_prefix = f"/{ep_name}"
124
+
125
+ # Factory target for uvicorn --factory
126
+ factory_target = f"{module_path}:create_standalone_app"
127
+
128
+ # Version: static or None if dynamic
129
+ version = project.get("version")
130
+
131
+ has_frontend = (project_dir / "frontend" / "package.json").exists()
132
+
133
+ return DiscoveredPlugin(
134
+ entry_point_name=ep_name,
135
+ module_path=module_path,
136
+ class_name=class_name,
137
+ factory_target=factory_target,
138
+ routes_prefix=routes_prefix,
139
+ project_name=project["name"],
140
+ project_version=version,
141
+ project_description=project.get("description", ""),
142
+ has_frontend=has_frontend,
143
+ )
144
+
145
+
146
+ def get_sdk_dependency_spec(project_dir: Path) -> str | None:
147
+ """Extract the mint-sdk version specifier from pyproject.toml dependencies.
148
+
149
+ Returns e.g. ">=0.9.4" or None if mint-sdk is not in dependencies.
150
+ """
151
+ data = _read_full_pyproject(project_dir)
152
+ deps = data.get("project", {}).get("dependencies", [])
153
+ for dep in deps:
154
+ # Match "mint-sdk>=0.9.4", "mint-sdk>=0.9.4,<1.0", "mint-sdk"
155
+ match = re.match(r"mint[-_]sdk\s*(.*)", dep, re.IGNORECASE)
156
+ if match:
157
+ spec = match.group(1).strip()
158
+ return spec if spec else "*"
159
+ return None
mint_sdk/_prompt.py ADDED
@@ -0,0 +1,228 @@
1
+ """Interactive terminal prompts with arrow-key selection (gh-style).
2
+
3
+ Zero external dependencies — uses ``tty``/``termios`` for raw input on
4
+ Unix systems. Falls back to plain ``input()`` when stdin is not a TTY
5
+ or on unsupported platforms.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+
12
+ # ANSI helpers
13
+ _BOLD = "\033[1m"
14
+ _CYAN = "\033[36m"
15
+ _GREEN = "\033[32m"
16
+ _DIM = "\033[2m"
17
+ _RESET = "\033[0m"
18
+ _CLEAR_LINE = "\033[2K"
19
+ _HIDE_CURSOR = "\033[?25l"
20
+ _SHOW_CURSOR = "\033[?25h"
21
+
22
+
23
+ def _is_interactive() -> bool:
24
+ """Return True when stdin/stdout are both TTYs."""
25
+ try:
26
+ return sys.stdin.isatty() and sys.stdout.isatty()
27
+ except Exception:
28
+ return False
29
+
30
+
31
+ def _read_key() -> str:
32
+ """Read a single keypress (or escape sequence) in raw mode."""
33
+ import termios
34
+ import tty
35
+
36
+ fd = sys.stdin.fileno()
37
+ old = termios.tcgetattr(fd)
38
+ try:
39
+ tty.setraw(fd)
40
+ ch = sys.stdin.read(1)
41
+ if ch == "\x1b":
42
+ ch2 = sys.stdin.read(1)
43
+ if ch2 == "[":
44
+ ch3 = sys.stdin.read(1)
45
+ return f"\x1b[{ch3}"
46
+ return ch + ch2
47
+ return ch
48
+ finally:
49
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
50
+
51
+
52
+ def prompt_text(label: str, default: str = "") -> str:
53
+ """Prompt for free-form text input with a default value.
54
+
55
+ Example output::
56
+
57
+ ? Plugin name (My Plugin):
58
+ """
59
+ if not _is_interactive():
60
+ return default
61
+
62
+ suffix = f" ({default})" if default else ""
63
+ try:
64
+ answer = input(f"{_GREEN}?{_RESET} {_BOLD}{label}{_RESET}{_DIM}{suffix}{_RESET}: ").strip()
65
+ except (EOFError, KeyboardInterrupt):
66
+ print()
67
+ sys.exit(1)
68
+ return answer or default
69
+
70
+
71
+ def prompt_select(label: str, choices: list[str], default: int = 0) -> int:
72
+ """Arrow-key selection from a list of choices. Returns the chosen index.
73
+
74
+ Example output::
75
+
76
+ ? Plugin type:
77
+ > Analysis
78
+ Experiment Design
79
+
80
+ Falls back to numbered input when not interactive.
81
+ """
82
+ if not _is_interactive():
83
+ return default
84
+
85
+ cursor = default
86
+
87
+ def _render(first: bool = False) -> None:
88
+ if not first:
89
+ # Move cursor up to overwrite previous render
90
+ sys.stdout.write(f"\033[{len(choices)}A")
91
+ for i, choice in enumerate(choices):
92
+ sys.stdout.write(_CLEAR_LINE)
93
+ if i == cursor:
94
+ sys.stdout.write(f" {_CYAN}> {choice}{_RESET}\n")
95
+ else:
96
+ sys.stdout.write(f" {choice}\n")
97
+ sys.stdout.flush()
98
+
99
+ sys.stdout.write(f"{_GREEN}?{_RESET} {_BOLD}{label}{_RESET}\n")
100
+ sys.stdout.write(_HIDE_CURSOR)
101
+ sys.stdout.flush()
102
+
103
+ try:
104
+ _render(first=True)
105
+ while True:
106
+ key = _read_key()
107
+ if key == "\x1b[A": # Up
108
+ cursor = (cursor - 1) % len(choices)
109
+ _render()
110
+ elif key == "\x1b[B": # Down
111
+ cursor = (cursor + 1) % len(choices)
112
+ _render()
113
+ elif key in ("\r", "\n"): # Enter
114
+ break
115
+ elif key == "\x03": # Ctrl+C
116
+ raise KeyboardInterrupt
117
+ except (KeyboardInterrupt, EOFError):
118
+ sys.stdout.write(_SHOW_CURSOR)
119
+ sys.stdout.flush()
120
+ print()
121
+ sys.exit(1)
122
+
123
+ # Replace the choice list with the final answer
124
+ sys.stdout.write(f"\033[{len(choices)}A")
125
+ for _ in choices:
126
+ sys.stdout.write(f"{_CLEAR_LINE}\n")
127
+ sys.stdout.write(f"\033[{len(choices)}A")
128
+ sys.stdout.write(f" {_CYAN}{choices[cursor]}{_RESET}\n")
129
+ sys.stdout.write(_SHOW_CURSOR)
130
+ sys.stdout.flush()
131
+
132
+ return cursor
133
+
134
+
135
+ def prompt_multiselect(
136
+ label: str,
137
+ choices: list[str],
138
+ defaults: list[int] | None = None,
139
+ ) -> list[int]:
140
+ """Arrow-key multi-select with space to toggle. Returns selected indices.
141
+
142
+ Example output::
143
+
144
+ ? AI coding assistant (space = toggle, enter = confirm)
145
+ [x] Claude Code
146
+ [ ] Codex (OpenAI)
147
+ [x] Cursor
148
+ [ ] Windsurf
149
+ [ ] None
150
+
151
+ Falls back to returning *defaults* when not interactive.
152
+ """
153
+ if not _is_interactive():
154
+ return list(defaults) if defaults else []
155
+
156
+ selected = set(defaults) if defaults else set()
157
+ cursor = 0
158
+
159
+ hint = f"{_DIM} (space = toggle, enter = confirm){_RESET}"
160
+
161
+ def _render(first: bool = False) -> None:
162
+ if not first:
163
+ sys.stdout.write(f"\033[{len(choices)}A")
164
+ for i, choice in enumerate(choices):
165
+ sys.stdout.write(_CLEAR_LINE)
166
+ check = "x" if i in selected else " "
167
+ if i == cursor:
168
+ sys.stdout.write(f" {_CYAN}>[{check}] {choice}{_RESET}\n")
169
+ else:
170
+ sys.stdout.write(f" [{check}] {choice}\n")
171
+ sys.stdout.flush()
172
+
173
+ sys.stdout.write(f"{_GREEN}?{_RESET} {_BOLD}{label}{_RESET}{hint}\n")
174
+ sys.stdout.write(_HIDE_CURSOR)
175
+ sys.stdout.flush()
176
+
177
+ try:
178
+ _render(first=True)
179
+ while True:
180
+ key = _read_key()
181
+ if key == "\x1b[A": # Up
182
+ cursor = (cursor - 1) % len(choices)
183
+ _render()
184
+ elif key == "\x1b[B": # Down
185
+ cursor = (cursor + 1) % len(choices)
186
+ _render()
187
+ elif key == " ": # Space — toggle
188
+ if cursor in selected:
189
+ selected.discard(cursor)
190
+ else:
191
+ selected.add(cursor)
192
+ _render()
193
+ elif key in ("\r", "\n"): # Enter — confirm
194
+ break
195
+ elif key == "\x03": # Ctrl+C
196
+ raise KeyboardInterrupt
197
+ except (KeyboardInterrupt, EOFError):
198
+ sys.stdout.write(_SHOW_CURSOR)
199
+ sys.stdout.flush()
200
+ print()
201
+ sys.exit(1)
202
+
203
+ # Collapse to summary line
204
+ sys.stdout.write(f"\033[{len(choices)}A")
205
+ for _ in choices:
206
+ sys.stdout.write(f"{_CLEAR_LINE}\n")
207
+ sys.stdout.write(f"\033[{len(choices)}A")
208
+ picked = [choices[i] for i in sorted(selected)]
209
+ summary = ", ".join(picked) if picked else "None"
210
+ sys.stdout.write(f" {_CYAN}{summary}{_RESET}\n")
211
+ sys.stdout.write(_SHOW_CURSOR)
212
+ sys.stdout.flush()
213
+
214
+ return sorted(selected)
215
+
216
+
217
+ def prompt_confirm(label: str, default: bool = True) -> bool:
218
+ """Yes/No selection with arrow keys.
219
+
220
+ Example output::
221
+
222
+ ? Include frontend?
223
+ > Yes
224
+ No
225
+ """
226
+ choices = ["Yes", "No"]
227
+ idx = prompt_select(label, choices, default=0 if default else 1)
228
+ return idx == 0
mint_sdk/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.0.0a1'
22
+ __version_tuple__ = version_tuple = (1, 0, 0, 'a1')
23
+
24
+ __commit_id__ = commit_id = None