synapse-sdk 1.0.0a11__py3-none-any.whl → 2026.1.1b2__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.
Potentially problematic release.
This version of synapse-sdk might be problematic. Click here for more details.
- synapse_sdk/__init__.py +24 -0
- synapse_sdk/cli/__init__.py +9 -8
- synapse_sdk/cli/agent/__init__.py +25 -0
- synapse_sdk/cli/agent/config.py +104 -0
- synapse_sdk/cli/agent/select.py +197 -0
- synapse_sdk/cli/auth.py +104 -0
- synapse_sdk/cli/main.py +1025 -0
- synapse_sdk/cli/plugin/__init__.py +58 -0
- synapse_sdk/cli/plugin/create.py +566 -0
- synapse_sdk/cli/plugin/job.py +196 -0
- synapse_sdk/cli/plugin/publish.py +322 -0
- synapse_sdk/cli/plugin/run.py +131 -0
- synapse_sdk/cli/plugin/test.py +200 -0
- synapse_sdk/clients/README.md +239 -0
- synapse_sdk/clients/__init__.py +5 -0
- synapse_sdk/clients/_template.py +266 -0
- synapse_sdk/clients/agent/__init__.py +84 -29
- synapse_sdk/clients/agent/async_ray.py +289 -0
- synapse_sdk/clients/agent/container.py +83 -0
- synapse_sdk/clients/agent/plugin.py +101 -0
- synapse_sdk/clients/agent/ray.py +296 -39
- synapse_sdk/clients/backend/__init__.py +152 -12
- synapse_sdk/clients/backend/annotation.py +164 -22
- synapse_sdk/clients/backend/core.py +101 -0
- synapse_sdk/clients/backend/data_collection.py +292 -0
- synapse_sdk/clients/backend/hitl.py +87 -0
- synapse_sdk/clients/backend/integration.py +374 -46
- synapse_sdk/clients/backend/ml.py +134 -22
- synapse_sdk/clients/backend/models.py +247 -0
- synapse_sdk/clients/base.py +538 -59
- synapse_sdk/clients/exceptions.py +35 -7
- synapse_sdk/clients/pipeline/__init__.py +5 -0
- synapse_sdk/clients/pipeline/client.py +636 -0
- synapse_sdk/clients/protocols.py +178 -0
- synapse_sdk/clients/utils.py +86 -8
- synapse_sdk/clients/validation.py +58 -0
- synapse_sdk/enums.py +76 -0
- synapse_sdk/exceptions.py +168 -0
- synapse_sdk/integrations/__init__.py +74 -0
- synapse_sdk/integrations/_base.py +119 -0
- synapse_sdk/integrations/_context.py +53 -0
- synapse_sdk/integrations/ultralytics/__init__.py +78 -0
- synapse_sdk/integrations/ultralytics/_callbacks.py +126 -0
- synapse_sdk/integrations/ultralytics/_patches.py +124 -0
- synapse_sdk/loggers.py +476 -95
- synapse_sdk/mcp/MCP.md +69 -0
- synapse_sdk/mcp/__init__.py +48 -0
- synapse_sdk/mcp/__main__.py +6 -0
- synapse_sdk/mcp/config.py +349 -0
- synapse_sdk/mcp/prompts/__init__.py +4 -0
- synapse_sdk/mcp/resources/__init__.py +4 -0
- synapse_sdk/mcp/server.py +1352 -0
- synapse_sdk/mcp/tools/__init__.py +6 -0
- synapse_sdk/plugins/__init__.py +133 -9
- synapse_sdk/plugins/action.py +229 -0
- synapse_sdk/plugins/actions/__init__.py +82 -0
- synapse_sdk/plugins/actions/dataset/__init__.py +37 -0
- synapse_sdk/plugins/actions/dataset/action.py +471 -0
- synapse_sdk/plugins/actions/export/__init__.py +55 -0
- synapse_sdk/plugins/actions/export/action.py +183 -0
- synapse_sdk/plugins/actions/export/context.py +59 -0
- synapse_sdk/plugins/actions/inference/__init__.py +84 -0
- synapse_sdk/plugins/actions/inference/action.py +285 -0
- synapse_sdk/plugins/actions/inference/context.py +81 -0
- synapse_sdk/plugins/actions/inference/deployment.py +322 -0
- synapse_sdk/plugins/actions/inference/serve.py +252 -0
- synapse_sdk/plugins/actions/train/__init__.py +54 -0
- synapse_sdk/plugins/actions/train/action.py +326 -0
- synapse_sdk/plugins/actions/train/context.py +57 -0
- synapse_sdk/plugins/actions/upload/__init__.py +49 -0
- synapse_sdk/plugins/actions/upload/action.py +165 -0
- synapse_sdk/plugins/actions/upload/context.py +61 -0
- synapse_sdk/plugins/config.py +98 -0
- synapse_sdk/plugins/context/__init__.py +109 -0
- synapse_sdk/plugins/context/env.py +113 -0
- synapse_sdk/plugins/datasets/__init__.py +113 -0
- synapse_sdk/plugins/datasets/converters/__init__.py +76 -0
- synapse_sdk/plugins/datasets/converters/base.py +347 -0
- synapse_sdk/plugins/datasets/converters/yolo/__init__.py +9 -0
- synapse_sdk/plugins/datasets/converters/yolo/from_dm.py +468 -0
- synapse_sdk/plugins/datasets/converters/yolo/to_dm.py +381 -0
- synapse_sdk/plugins/datasets/formats/__init__.py +82 -0
- synapse_sdk/plugins/datasets/formats/dm.py +351 -0
- synapse_sdk/plugins/datasets/formats/yolo.py +240 -0
- synapse_sdk/plugins/decorators.py +83 -0
- synapse_sdk/plugins/discovery.py +790 -0
- synapse_sdk/plugins/docs/ACTION_DEV_GUIDE.md +933 -0
- synapse_sdk/plugins/docs/ARCHITECTURE.md +1225 -0
- synapse_sdk/plugins/docs/LOGGING_SYSTEM.md +683 -0
- synapse_sdk/plugins/docs/OVERVIEW.md +531 -0
- synapse_sdk/plugins/docs/PIPELINE_GUIDE.md +145 -0
- synapse_sdk/plugins/docs/README.md +513 -0
- synapse_sdk/plugins/docs/STEP.md +656 -0
- synapse_sdk/plugins/enums.py +70 -10
- synapse_sdk/plugins/errors.py +92 -0
- synapse_sdk/plugins/executors/__init__.py +43 -0
- synapse_sdk/plugins/executors/local.py +99 -0
- synapse_sdk/plugins/executors/ray/__init__.py +18 -0
- synapse_sdk/plugins/executors/ray/base.py +282 -0
- synapse_sdk/plugins/executors/ray/job.py +298 -0
- synapse_sdk/plugins/executors/ray/jobs_api.py +511 -0
- synapse_sdk/plugins/executors/ray/packaging.py +137 -0
- synapse_sdk/plugins/executors/ray/pipeline.py +792 -0
- synapse_sdk/plugins/executors/ray/task.py +257 -0
- synapse_sdk/plugins/models/__init__.py +26 -0
- synapse_sdk/plugins/models/logger.py +173 -0
- synapse_sdk/plugins/models/pipeline.py +25 -0
- synapse_sdk/plugins/pipelines/__init__.py +81 -0
- synapse_sdk/plugins/pipelines/action_pipeline.py +417 -0
- synapse_sdk/plugins/pipelines/context.py +107 -0
- synapse_sdk/plugins/pipelines/display.py +311 -0
- synapse_sdk/plugins/runner.py +114 -0
- synapse_sdk/plugins/schemas/__init__.py +19 -0
- synapse_sdk/plugins/schemas/results.py +152 -0
- synapse_sdk/plugins/steps/__init__.py +63 -0
- synapse_sdk/plugins/steps/base.py +128 -0
- synapse_sdk/plugins/steps/context.py +90 -0
- synapse_sdk/plugins/steps/orchestrator.py +128 -0
- synapse_sdk/plugins/steps/registry.py +103 -0
- synapse_sdk/plugins/steps/utils/__init__.py +20 -0
- synapse_sdk/plugins/steps/utils/logging.py +85 -0
- synapse_sdk/plugins/steps/utils/timing.py +71 -0
- synapse_sdk/plugins/steps/utils/validation.py +68 -0
- synapse_sdk/plugins/templates/__init__.py +50 -0
- synapse_sdk/plugins/templates/base/.gitignore.j2 +26 -0
- synapse_sdk/plugins/templates/base/.synapseignore.j2 +11 -0
- synapse_sdk/plugins/templates/base/README.md.j2 +26 -0
- synapse_sdk/plugins/templates/base/plugin/__init__.py.j2 +1 -0
- synapse_sdk/plugins/templates/base/pyproject.toml.j2 +14 -0
- synapse_sdk/plugins/templates/base/requirements.txt.j2 +1 -0
- synapse_sdk/plugins/templates/custom/plugin/main.py.j2 +18 -0
- synapse_sdk/plugins/templates/data_validation/plugin/validate.py.j2 +32 -0
- synapse_sdk/plugins/templates/export/plugin/export.py.j2 +36 -0
- synapse_sdk/plugins/templates/neural_net/plugin/inference.py.j2 +36 -0
- synapse_sdk/plugins/templates/neural_net/plugin/train.py.j2 +33 -0
- synapse_sdk/plugins/templates/post_annotation/plugin/post_annotate.py.j2 +32 -0
- synapse_sdk/plugins/templates/pre_annotation/plugin/pre_annotate.py.j2 +32 -0
- synapse_sdk/plugins/templates/smart_tool/plugin/auto_label.py.j2 +44 -0
- synapse_sdk/plugins/templates/upload/plugin/upload.py.j2 +35 -0
- synapse_sdk/plugins/testing/__init__.py +25 -0
- synapse_sdk/plugins/testing/sample_actions.py +98 -0
- synapse_sdk/plugins/types.py +206 -0
- synapse_sdk/plugins/upload.py +595 -64
- synapse_sdk/plugins/utils.py +325 -37
- synapse_sdk/shared/__init__.py +25 -0
- synapse_sdk/utils/__init__.py +1 -0
- synapse_sdk/utils/auth.py +74 -0
- synapse_sdk/utils/file/__init__.py +58 -0
- synapse_sdk/utils/file/archive.py +449 -0
- synapse_sdk/utils/file/checksum.py +167 -0
- synapse_sdk/utils/file/download.py +286 -0
- synapse_sdk/utils/file/io.py +129 -0
- synapse_sdk/utils/file/requirements.py +36 -0
- synapse_sdk/utils/network.py +168 -0
- synapse_sdk/utils/storage/__init__.py +238 -0
- synapse_sdk/utils/storage/config.py +188 -0
- synapse_sdk/utils/storage/errors.py +52 -0
- synapse_sdk/utils/storage/providers/__init__.py +13 -0
- synapse_sdk/utils/storage/providers/base.py +76 -0
- synapse_sdk/utils/storage/providers/gcs.py +168 -0
- synapse_sdk/utils/storage/providers/http.py +250 -0
- synapse_sdk/utils/storage/providers/local.py +126 -0
- synapse_sdk/utils/storage/providers/s3.py +177 -0
- synapse_sdk/utils/storage/providers/sftp.py +208 -0
- synapse_sdk/utils/storage/registry.py +125 -0
- synapse_sdk/utils/websocket.py +99 -0
- synapse_sdk-2026.1.1b2.dist-info/METADATA +715 -0
- synapse_sdk-2026.1.1b2.dist-info/RECORD +172 -0
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/WHEEL +1 -1
- synapse_sdk-2026.1.1b2.dist-info/licenses/LICENSE +201 -0
- locale/en/LC_MESSAGES/messages.mo +0 -0
- locale/en/LC_MESSAGES/messages.po +0 -39
- locale/ko/LC_MESSAGES/messages.mo +0 -0
- locale/ko/LC_MESSAGES/messages.po +0 -34
- synapse_sdk/cli/create_plugin.py +0 -10
- synapse_sdk/clients/agent/core.py +0 -7
- synapse_sdk/clients/agent/service.py +0 -15
- synapse_sdk/clients/backend/dataset.py +0 -51
- synapse_sdk/clients/ray/__init__.py +0 -6
- synapse_sdk/clients/ray/core.py +0 -22
- synapse_sdk/clients/ray/serve.py +0 -20
- synapse_sdk/i18n.py +0 -35
- synapse_sdk/plugins/categories/__init__.py +0 -0
- synapse_sdk/plugins/categories/base.py +0 -235
- synapse_sdk/plugins/categories/data_validation/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/actions/validation.py +0 -10
- synapse_sdk/plugins/categories/data_validation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/data_validation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +0 -5
- synapse_sdk/plugins/categories/decorators.py +0 -13
- synapse_sdk/plugins/categories/export/__init__.py +0 -0
- synapse_sdk/plugins/categories/export/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/export/actions/export.py +0 -10
- synapse_sdk/plugins/categories/import/__init__.py +0 -0
- synapse_sdk/plugins/categories/import/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/import/actions/import.py +0 -10
- synapse_sdk/plugins/categories/neural_net/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/actions/deployment.py +0 -45
- synapse_sdk/plugins/categories/neural_net/actions/inference.py +0 -18
- synapse_sdk/plugins/categories/neural_net/actions/test.py +0 -10
- synapse_sdk/plugins/categories/neural_net/actions/train.py +0 -143
- synapse_sdk/plugins/categories/neural_net/templates/config.yaml +0 -12
- synapse_sdk/plugins/categories/neural_net/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +0 -4
- synapse_sdk/plugins/categories/neural_net/templates/plugin/test.py +0 -2
- synapse_sdk/plugins/categories/neural_net/templates/plugin/train.py +0 -14
- synapse_sdk/plugins/categories/post_annotation/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/actions/post_annotation.py +0 -10
- synapse_sdk/plugins/categories/post_annotation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/post_annotation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.py +0 -3
- synapse_sdk/plugins/categories/pre_annotation/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py +0 -10
- synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py +0 -3
- synapse_sdk/plugins/categories/registry.py +0 -16
- synapse_sdk/plugins/categories/smart_tool/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/actions/auto_label.py +0 -37
- synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +0 -7
- synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py +0 -11
- synapse_sdk/plugins/categories/templates.py +0 -32
- synapse_sdk/plugins/cli/__init__.py +0 -21
- synapse_sdk/plugins/cli/publish.py +0 -37
- synapse_sdk/plugins/cli/run.py +0 -67
- synapse_sdk/plugins/exceptions.py +0 -22
- synapse_sdk/plugins/models.py +0 -121
- synapse_sdk/plugins/templates/cookiecutter.json +0 -11
- synapse_sdk/plugins/templates/hooks/post_gen_project.py +0 -3
- synapse_sdk/plugins/templates/hooks/pre_prompt.py +0 -21
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.gitignore +0 -27
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.pre-commit-config.yaml +0 -7
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/README.md +0 -5
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +0 -6
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/plugin/__init__.py +0 -0
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/pyproject.toml +0 -13
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +0 -1
- synapse_sdk/shared/enums.py +0 -8
- synapse_sdk/utils/debug.py +0 -5
- synapse_sdk/utils/file.py +0 -87
- synapse_sdk/utils/module_loading.py +0 -29
- synapse_sdk/utils/pydantic/__init__.py +0 -0
- synapse_sdk/utils/pydantic/config.py +0 -4
- synapse_sdk/utils/pydantic/errors.py +0 -33
- synapse_sdk/utils/pydantic/validators.py +0 -7
- synapse_sdk/utils/storage.py +0 -91
- synapse_sdk/utils/string.py +0 -11
- synapse_sdk-1.0.0a11.dist-info/LICENSE +0 -21
- synapse_sdk-1.0.0a11.dist-info/METADATA +0 -43
- synapse_sdk-1.0.0a11.dist-info/RECORD +0 -111
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
"""Plugin discovery and introspection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import inspect
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from synapse_sdk.plugins.config import ActionConfig, PluginConfig
|
|
14
|
+
from synapse_sdk.plugins.enums import PluginCategory, RunMethod
|
|
15
|
+
from synapse_sdk.plugins.errors import ActionNotFoundError
|
|
16
|
+
from synapse_sdk.plugins.utils import pydantic_to_ui_schema
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from synapse_sdk.plugins.action import BaseAction
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PluginDiscovery:
|
|
23
|
+
"""Plugin discovery and introspection.
|
|
24
|
+
|
|
25
|
+
Provides methods to discover actions from configuration files or Python modules.
|
|
26
|
+
Supports both class-based (BaseAction subclasses) and function-based (@action decorator)
|
|
27
|
+
action definitions.
|
|
28
|
+
|
|
29
|
+
Example from config:
|
|
30
|
+
>>> discovery = PluginDiscovery.from_path('/path/to/plugin')
|
|
31
|
+
>>> discovery.list_actions()
|
|
32
|
+
['train', 'inference', 'export']
|
|
33
|
+
>>> action_cls = discovery.get_action_class('train')
|
|
34
|
+
|
|
35
|
+
Example from module:
|
|
36
|
+
>>> import my_plugin
|
|
37
|
+
>>> discovery = PluginDiscovery.from_module(my_plugin)
|
|
38
|
+
>>> discovery.list_actions()
|
|
39
|
+
['train', 'export']
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config: PluginConfig) -> None:
|
|
43
|
+
"""Initialize discovery with a plugin configuration.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config: Validated PluginConfig instance
|
|
47
|
+
"""
|
|
48
|
+
self.config = config
|
|
49
|
+
self._action_cache: dict[str, type[BaseAction] | Callable] = {}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_path(cls, path: Path | str) -> PluginDiscovery:
|
|
53
|
+
"""Load plugin from config.yaml path.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
path: Path to config.yaml file or directory containing it
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
PluginDiscovery instance
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
FileNotFoundError: If config.yaml doesn't exist
|
|
63
|
+
ValueError: If config.yaml is invalid
|
|
64
|
+
"""
|
|
65
|
+
import yaml
|
|
66
|
+
|
|
67
|
+
path = Path(path)
|
|
68
|
+
if path.is_dir():
|
|
69
|
+
path = path / 'config.yaml'
|
|
70
|
+
|
|
71
|
+
if not path.exists():
|
|
72
|
+
raise FileNotFoundError(f'Config file not found: {path}')
|
|
73
|
+
|
|
74
|
+
with path.open() as f:
|
|
75
|
+
data = yaml.safe_load(f)
|
|
76
|
+
|
|
77
|
+
# Convert actions dict to ActionConfig objects if needed
|
|
78
|
+
if 'actions' in data and isinstance(data['actions'], dict):
|
|
79
|
+
actions = {}
|
|
80
|
+
for action_name, action_data in data['actions'].items():
|
|
81
|
+
if isinstance(action_data, dict):
|
|
82
|
+
action_data.setdefault('name', action_name)
|
|
83
|
+
actions[action_name] = ActionConfig(**action_data)
|
|
84
|
+
elif isinstance(action_data, ActionConfig):
|
|
85
|
+
actions[action_name] = action_data
|
|
86
|
+
data['actions'] = actions
|
|
87
|
+
|
|
88
|
+
config = PluginConfig(**data)
|
|
89
|
+
return cls(config)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_module(
|
|
93
|
+
cls,
|
|
94
|
+
module: ModuleType,
|
|
95
|
+
*,
|
|
96
|
+
name: str | None = None,
|
|
97
|
+
category: PluginCategory = PluginCategory.CUSTOM,
|
|
98
|
+
) -> PluginDiscovery:
|
|
99
|
+
"""Discover plugin from Python module by introspection.
|
|
100
|
+
|
|
101
|
+
Scans module for:
|
|
102
|
+
- Functions decorated with @action
|
|
103
|
+
- Classes that subclass BaseAction
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
module: Python module to introspect
|
|
107
|
+
name: Plugin name (defaults to module name)
|
|
108
|
+
category: Plugin category
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
PluginDiscovery instance with discovered actions
|
|
112
|
+
"""
|
|
113
|
+
from synapse_sdk.plugins.action import BaseAction
|
|
114
|
+
|
|
115
|
+
actions: dict[str, ActionConfig] = {}
|
|
116
|
+
|
|
117
|
+
for attr_name in dir(module):
|
|
118
|
+
if attr_name.startswith('_'):
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
obj = getattr(module, attr_name)
|
|
122
|
+
|
|
123
|
+
# Check for @action decorated functions
|
|
124
|
+
if callable(obj) and hasattr(obj, '_is_action') and obj._is_action:
|
|
125
|
+
action_name = getattr(obj, '_action_name', attr_name)
|
|
126
|
+
actions[action_name] = ActionConfig(
|
|
127
|
+
name=action_name,
|
|
128
|
+
description=getattr(obj, '_action_description', ''),
|
|
129
|
+
entrypoint=f'{module.__name__}:{attr_name}',
|
|
130
|
+
method=RunMethod.TASK,
|
|
131
|
+
params_schema=getattr(obj, '_action_params', None),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Check for BaseAction subclasses
|
|
135
|
+
elif (
|
|
136
|
+
inspect.isclass(obj)
|
|
137
|
+
and issubclass(obj, BaseAction)
|
|
138
|
+
and obj is not BaseAction
|
|
139
|
+
and hasattr(obj, 'action_name')
|
|
140
|
+
):
|
|
141
|
+
action_name = obj.action_name
|
|
142
|
+
actions[action_name] = ActionConfig(
|
|
143
|
+
name=action_name,
|
|
144
|
+
description=getattr(obj, 'description', ''),
|
|
145
|
+
entrypoint=f'{module.__name__}:{attr_name}',
|
|
146
|
+
method=getattr(obj, 'method', RunMethod.TASK),
|
|
147
|
+
params_schema=getattr(obj, 'params_model', None),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
module_name = name or getattr(module, '__name__', 'unknown')
|
|
151
|
+
config = PluginConfig(
|
|
152
|
+
name=module_name,
|
|
153
|
+
code=module_name.replace('.', '-'),
|
|
154
|
+
category=category,
|
|
155
|
+
actions=actions,
|
|
156
|
+
)
|
|
157
|
+
return cls(config)
|
|
158
|
+
|
|
159
|
+
def list_actions(self) -> list[str]:
|
|
160
|
+
"""Get available action names.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of action names
|
|
164
|
+
"""
|
|
165
|
+
return list(self.config.actions.keys())
|
|
166
|
+
|
|
167
|
+
def get_action_config(self, name: str) -> ActionConfig:
|
|
168
|
+
"""Get configuration for a specific action.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
name: Action name
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
ActionConfig instance
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
ActionNotFoundError: If action doesn't exist
|
|
178
|
+
"""
|
|
179
|
+
if name not in self.config.actions:
|
|
180
|
+
raise ActionNotFoundError(
|
|
181
|
+
f"Action '{name}' not found",
|
|
182
|
+
details={'available': self.list_actions()},
|
|
183
|
+
)
|
|
184
|
+
return self.config.actions[name]
|
|
185
|
+
|
|
186
|
+
def get_action_method(self, name: str) -> RunMethod:
|
|
187
|
+
"""Get execution method for an action.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
name: Action name
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
RunMethod enum value
|
|
194
|
+
"""
|
|
195
|
+
return self.get_action_config(name).method
|
|
196
|
+
|
|
197
|
+
def get_action_class(self, name: str) -> type[BaseAction] | Callable:
|
|
198
|
+
"""Load action class/function from entrypoint.
|
|
199
|
+
|
|
200
|
+
Injects action_name and category from config if not defined on the class.
|
|
201
|
+
This allows plugin developers to write minimal action classes without
|
|
202
|
+
redundant metadata when using config.yaml-based discovery.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
name: Action name
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Action class (BaseAction subclass) or decorated function
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ActionNotFoundError: If action doesn't exist or has no entrypoint
|
|
212
|
+
"""
|
|
213
|
+
if name in self._action_cache:
|
|
214
|
+
return self._action_cache[name]
|
|
215
|
+
|
|
216
|
+
action_config = self.get_action_config(name)
|
|
217
|
+
entrypoint = action_config.entrypoint
|
|
218
|
+
|
|
219
|
+
if not entrypoint:
|
|
220
|
+
raise ActionNotFoundError(
|
|
221
|
+
f"Action '{name}' has no entrypoint defined",
|
|
222
|
+
details={'action': name},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
action_cls = _load_entrypoint(entrypoint)
|
|
226
|
+
|
|
227
|
+
# Inject action_name from config key if not defined on class
|
|
228
|
+
if not getattr(action_cls, 'action_name', None):
|
|
229
|
+
action_cls.action_name = name
|
|
230
|
+
|
|
231
|
+
# Inject category from plugin config if not defined on class
|
|
232
|
+
if not getattr(action_cls, 'category', None):
|
|
233
|
+
action_cls.category = str(self.config.category)
|
|
234
|
+
|
|
235
|
+
self._action_cache[name] = action_cls
|
|
236
|
+
return action_cls
|
|
237
|
+
|
|
238
|
+
def has_action(self, name: str) -> bool:
|
|
239
|
+
"""Check if an action exists.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
name: Action name
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if action exists, False otherwise
|
|
246
|
+
"""
|
|
247
|
+
return name in self.config.actions
|
|
248
|
+
|
|
249
|
+
def get_action_entrypoint(self, name: str) -> str:
|
|
250
|
+
"""Get action entrypoint string without loading the class.
|
|
251
|
+
|
|
252
|
+
Useful for remote execution where the class shouldn't be imported locally.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
name: Action name
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Entrypoint string (e.g., 'plugin.train.TrainAction')
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ActionNotFoundError: If action doesn't exist or has no entrypoint
|
|
262
|
+
"""
|
|
263
|
+
action_config = self.get_action_config(name)
|
|
264
|
+
entrypoint = action_config.entrypoint
|
|
265
|
+
|
|
266
|
+
if not entrypoint:
|
|
267
|
+
raise ActionNotFoundError(
|
|
268
|
+
f"Action '{name}' has no entrypoint defined",
|
|
269
|
+
details={'action': name},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Normalize entrypoint format: 'module:Class' -> 'module.Class'
|
|
273
|
+
if ':' in entrypoint:
|
|
274
|
+
module_path, class_name = entrypoint.rsplit(':', 1)
|
|
275
|
+
return f'{module_path}.{class_name}'
|
|
276
|
+
return entrypoint
|
|
277
|
+
|
|
278
|
+
def get_action_params_model(self, name: str) -> type[BaseModel] | None:
|
|
279
|
+
"""Get the params model for an action.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
name: Action name
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Pydantic model class for parameters, or None if not defined
|
|
286
|
+
"""
|
|
287
|
+
action_config = self.get_action_config(name)
|
|
288
|
+
|
|
289
|
+
# First check if params_schema is set on ActionConfig
|
|
290
|
+
if action_config.params_schema:
|
|
291
|
+
return action_config.params_schema
|
|
292
|
+
|
|
293
|
+
# Otherwise try to load from action class
|
|
294
|
+
try:
|
|
295
|
+
action_cls = self.get_action_class(name)
|
|
296
|
+
return getattr(action_cls, 'params_model', None)
|
|
297
|
+
except Exception:
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
def get_action_ui_schema(self, name: str) -> list[dict[str, Any]]:
|
|
301
|
+
"""Get UI schema for an action's parameters.
|
|
302
|
+
|
|
303
|
+
Auto-generates FormKit-compatible UI schema from the action's params_model.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
name: Action name
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
List of FormKit schema items, or empty list if no params_model
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
>>> discovery = PluginDiscovery.from_path('/path/to/plugin')
|
|
313
|
+
>>> schema = discovery.get_action_ui_schema('train')
|
|
314
|
+
>>> schema
|
|
315
|
+
[{'$formkit': 'number', 'name': 'epochs', 'label': 'Epochs', ...}]
|
|
316
|
+
"""
|
|
317
|
+
params_model = self.get_action_params_model(name)
|
|
318
|
+
if params_model is None:
|
|
319
|
+
return []
|
|
320
|
+
return pydantic_to_ui_schema(params_model)
|
|
321
|
+
|
|
322
|
+
def get_action_result_model(self, name: str) -> type[BaseModel] | None:
|
|
323
|
+
"""Get the result model for an action.
|
|
324
|
+
|
|
325
|
+
Returns the Pydantic model class used for output validation.
|
|
326
|
+
Returns None if no result model is defined (NoResult sentinel).
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
name: Action name
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Pydantic model class for result validation, or None if not defined
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> discovery = PluginDiscovery.from_path('/path/to/plugin')
|
|
336
|
+
>>> result_model = discovery.get_action_result_model('train')
|
|
337
|
+
>>> if result_model:
|
|
338
|
+
... print(result_model.model_json_schema())
|
|
339
|
+
"""
|
|
340
|
+
from synapse_sdk.plugins.action import NoResult
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
action_cls = self.get_action_class(name)
|
|
344
|
+
result_model = getattr(action_cls, 'result_model', NoResult)
|
|
345
|
+
if result_model is NoResult:
|
|
346
|
+
return None
|
|
347
|
+
return result_model
|
|
348
|
+
except Exception:
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
def get_action_result_ui_schema(self, name: str) -> list[dict[str, Any]]:
|
|
352
|
+
"""Get UI schema for an action's result type.
|
|
353
|
+
|
|
354
|
+
Auto-generates FormKit-compatible UI schema from the action's result_model.
|
|
355
|
+
Useful for displaying expected output format in UIs.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
name: Action name
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
List of FormKit schema items, or empty list if no result_model
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
>>> discovery = PluginDiscovery.from_path('/path/to/plugin')
|
|
365
|
+
>>> schema = discovery.get_action_result_ui_schema('train')
|
|
366
|
+
>>> schema
|
|
367
|
+
[{'$formkit': 'text', 'name': 'weights_path', 'label': 'Weights Path', ...}]
|
|
368
|
+
"""
|
|
369
|
+
result_model = self.get_action_result_model(name)
|
|
370
|
+
if result_model is None:
|
|
371
|
+
return []
|
|
372
|
+
return pydantic_to_ui_schema(result_model)
|
|
373
|
+
|
|
374
|
+
def get_action_input_type(self, name: str) -> str | None:
|
|
375
|
+
"""Get the semantic input type for an action.
|
|
376
|
+
|
|
377
|
+
Extracts the input_type from the action class's DataType declaration.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
name: Action name
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Input type name string, or None if not defined
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
>>> discovery = PluginDiscovery.from_path('/path/to/plugin')
|
|
387
|
+
>>> discovery.get_action_input_type('train')
|
|
388
|
+
'yolo_dataset'
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
action_cls = self.get_action_class(name)
|
|
392
|
+
input_type = getattr(action_cls, 'input_type', None)
|
|
393
|
+
if input_type is not None and hasattr(input_type, 'name'):
|
|
394
|
+
return input_type.name
|
|
395
|
+
return None
|
|
396
|
+
except Exception:
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
def get_action_output_type(self, name: str) -> str | None:
|
|
400
|
+
"""Get the semantic output type for an action.
|
|
401
|
+
|
|
402
|
+
Extracts the output_type from the action class's DataType declaration.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
name: Action name
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Output type name string, or None if not defined
|
|
409
|
+
|
|
410
|
+
Example:
|
|
411
|
+
>>> discovery = PluginDiscovery.from_path('/path/to/plugin')
|
|
412
|
+
>>> discovery.get_action_output_type('train')
|
|
413
|
+
'model_weights'
|
|
414
|
+
"""
|
|
415
|
+
try:
|
|
416
|
+
action_cls = self.get_action_class(name)
|
|
417
|
+
output_type = getattr(action_cls, 'output_type', None)
|
|
418
|
+
if output_type is not None and hasattr(output_type, 'name'):
|
|
419
|
+
return output_type.name
|
|
420
|
+
return None
|
|
421
|
+
except Exception:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
def to_config_dict(self, *, include_ui_schemas: bool = True) -> dict[str, Any]:
|
|
425
|
+
"""Export plugin configuration as a dictionary.
|
|
426
|
+
|
|
427
|
+
Generates a config dict compatible with the backend API format,
|
|
428
|
+
with optional auto-generation of UI schemas from params_model.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
include_ui_schemas: If True, auto-generate train_ui_schemas
|
|
432
|
+
from each action's params_model
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Config dictionary ready for serialization or API submission
|
|
436
|
+
|
|
437
|
+
Example:
|
|
438
|
+
>>> discovery = PluginDiscovery.from_module(my_plugin)
|
|
439
|
+
>>> config = discovery.to_config_dict()
|
|
440
|
+
>>> # config['actions']['train']['hyperparameters']['train_ui_schemas']
|
|
441
|
+
>>> # is auto-populated from TrainParams model
|
|
442
|
+
"""
|
|
443
|
+
config_dict: dict[str, Any] = {
|
|
444
|
+
'name': self.config.name,
|
|
445
|
+
'code': self.config.code,
|
|
446
|
+
'version': self.config.version,
|
|
447
|
+
'category': str(self.config.category.value),
|
|
448
|
+
'description': self.config.description,
|
|
449
|
+
'readme': self.config.readme,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Add optional fields
|
|
453
|
+
if self.config.data_type:
|
|
454
|
+
config_dict['data_type'] = str(self.config.data_type.value)
|
|
455
|
+
if self.config.tasks:
|
|
456
|
+
config_dict['tasks'] = self.config.tasks
|
|
457
|
+
|
|
458
|
+
# Build actions dict
|
|
459
|
+
actions_dict: dict[str, Any] = {}
|
|
460
|
+
for action_name, action_config in self.config.actions.items():
|
|
461
|
+
action_dict: dict[str, Any] = {
|
|
462
|
+
'entrypoint': action_config.entrypoint,
|
|
463
|
+
'method': str(action_config.method.value),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if action_config.description:
|
|
467
|
+
action_dict['description'] = action_config.description
|
|
468
|
+
|
|
469
|
+
# Add semantic types (extracted from action class)
|
|
470
|
+
input_type = self.get_action_input_type(action_name)
|
|
471
|
+
output_type = self.get_action_output_type(action_name)
|
|
472
|
+
if input_type:
|
|
473
|
+
action_dict['input_type'] = input_type
|
|
474
|
+
if output_type:
|
|
475
|
+
action_dict['output_type'] = output_type
|
|
476
|
+
|
|
477
|
+
# Auto-generate UI schemas from params_model
|
|
478
|
+
if include_ui_schemas:
|
|
479
|
+
ui_schema = self.get_action_ui_schema(action_name)
|
|
480
|
+
if ui_schema:
|
|
481
|
+
action_dict['hyperparameters'] = {
|
|
482
|
+
'train_ui_schemas': ui_schema,
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Add result schema if defined
|
|
486
|
+
result_schema = self.get_action_result_ui_schema(action_name)
|
|
487
|
+
if result_schema:
|
|
488
|
+
action_dict['result_schema'] = result_schema
|
|
489
|
+
|
|
490
|
+
actions_dict[action_name] = action_dict
|
|
491
|
+
|
|
492
|
+
config_dict['actions'] = actions_dict
|
|
493
|
+
return config_dict
|
|
494
|
+
|
|
495
|
+
def to_yaml(self, *, include_ui_schemas: bool = True) -> str:
|
|
496
|
+
"""Export plugin configuration as YAML string.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
include_ui_schemas: If True, auto-generate train_ui_schemas
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
YAML-formatted configuration string
|
|
503
|
+
"""
|
|
504
|
+
import yaml
|
|
505
|
+
|
|
506
|
+
config_dict = self.to_config_dict(include_ui_schemas=include_ui_schemas)
|
|
507
|
+
return yaml.dump(config_dict, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
508
|
+
|
|
509
|
+
def sync_config_file(self, config_path: Path | str) -> dict[str, str]:
|
|
510
|
+
"""Sync actions and types from code to config.yaml.
|
|
511
|
+
|
|
512
|
+
Discovers actions from plugin source files and updates config.yaml:
|
|
513
|
+
- Adds new actions found in code but not in config
|
|
514
|
+
- Updates entrypoints if they changed
|
|
515
|
+
- Updates input_type/output_type from code
|
|
516
|
+
|
|
517
|
+
This should be called during plugin publish to ensure config.yaml
|
|
518
|
+
reflects the code-defined actions and types.
|
|
519
|
+
|
|
520
|
+
Args:
|
|
521
|
+
config_path: Path to config.yaml file
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Dict of action_name -> changes made (for logging)
|
|
525
|
+
|
|
526
|
+
Example:
|
|
527
|
+
>>> discovery = PluginDiscovery.from_path('/path/to/plugin')
|
|
528
|
+
>>> changes = discovery.sync_config_file('/path/to/plugin/config.yaml')
|
|
529
|
+
>>> print(changes)
|
|
530
|
+
{'train': 'added (entrypoint=plugin.train.TrainAction, input_type=yolo_dataset)'}
|
|
531
|
+
"""
|
|
532
|
+
import yaml
|
|
533
|
+
|
|
534
|
+
config_path = Path(config_path)
|
|
535
|
+
if not config_path.exists():
|
|
536
|
+
raise FileNotFoundError(f'Config file not found: {config_path}')
|
|
537
|
+
|
|
538
|
+
plugin_dir = config_path.parent
|
|
539
|
+
|
|
540
|
+
# Read existing config
|
|
541
|
+
with config_path.open() as f:
|
|
542
|
+
config_data = yaml.safe_load(f)
|
|
543
|
+
|
|
544
|
+
if config_data is None:
|
|
545
|
+
config_data = {}
|
|
546
|
+
|
|
547
|
+
changes: dict[str, str] = {}
|
|
548
|
+
|
|
549
|
+
# Ensure actions dict exists
|
|
550
|
+
if 'actions' not in config_data:
|
|
551
|
+
config_data['actions'] = {}
|
|
552
|
+
|
|
553
|
+
# Discover actions from source files
|
|
554
|
+
discovered = self.discover_actions(plugin_dir)
|
|
555
|
+
|
|
556
|
+
# Process discovered actions
|
|
557
|
+
for action_name, action_info in discovered.items():
|
|
558
|
+
entrypoint = action_info['entrypoint']
|
|
559
|
+
input_type = action_info['input_type']
|
|
560
|
+
output_type = action_info['output_type']
|
|
561
|
+
|
|
562
|
+
if action_name not in config_data['actions']:
|
|
563
|
+
# New action - add it
|
|
564
|
+
action_data: dict[str, Any] = {'entrypoint': entrypoint}
|
|
565
|
+
if input_type:
|
|
566
|
+
action_data['input_type'] = input_type
|
|
567
|
+
if output_type:
|
|
568
|
+
action_data['output_type'] = output_type
|
|
569
|
+
|
|
570
|
+
config_data['actions'][action_name] = action_data
|
|
571
|
+
|
|
572
|
+
parts = [f'entrypoint={entrypoint}']
|
|
573
|
+
if input_type:
|
|
574
|
+
parts.append(f'input_type={input_type}')
|
|
575
|
+
if output_type:
|
|
576
|
+
parts.append(f'output_type={output_type}')
|
|
577
|
+
changes[action_name] = f'added ({", ".join(parts)})'
|
|
578
|
+
else:
|
|
579
|
+
# Existing action - update fields if changed
|
|
580
|
+
action_data = config_data['actions'][action_name]
|
|
581
|
+
if not isinstance(action_data, dict):
|
|
582
|
+
action_data = {'entrypoint': action_data}
|
|
583
|
+
config_data['actions'][action_name] = action_data
|
|
584
|
+
|
|
585
|
+
updates = []
|
|
586
|
+
|
|
587
|
+
# Update entrypoint if changed
|
|
588
|
+
if action_data.get('entrypoint') != entrypoint:
|
|
589
|
+
action_data['entrypoint'] = entrypoint
|
|
590
|
+
updates.append(f'entrypoint={entrypoint}')
|
|
591
|
+
|
|
592
|
+
# Update types if changed
|
|
593
|
+
if input_type and action_data.get('input_type') != input_type:
|
|
594
|
+
action_data['input_type'] = input_type
|
|
595
|
+
updates.append(f'input_type={input_type}')
|
|
596
|
+
if output_type and action_data.get('output_type') != output_type:
|
|
597
|
+
action_data['output_type'] = output_type
|
|
598
|
+
updates.append(f'output_type={output_type}')
|
|
599
|
+
|
|
600
|
+
if updates:
|
|
601
|
+
changes[action_name] = ', '.join(updates)
|
|
602
|
+
|
|
603
|
+
# Write back to file
|
|
604
|
+
with config_path.open('w') as f:
|
|
605
|
+
yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
606
|
+
|
|
607
|
+
return changes
|
|
608
|
+
|
|
609
|
+
@staticmethod
|
|
610
|
+
def discover_actions(plugin_dir: Path | str) -> dict[str, dict[str, Any]]:
|
|
611
|
+
"""Discover BaseAction subclasses from plugin source files.
|
|
612
|
+
|
|
613
|
+
Scans Python files in the plugin directory for classes that inherit
|
|
614
|
+
from BaseAction and extracts their metadata.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
plugin_dir: Path to the plugin directory
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
Dict mapping action_name -> {entrypoint, input_type, output_type}
|
|
621
|
+
|
|
622
|
+
Example:
|
|
623
|
+
>>> actions = PluginDiscovery.discover_actions('/path/to/plugin')
|
|
624
|
+
>>> actions
|
|
625
|
+
{'train': {'entrypoint': 'plugin.train.TrainAction', 'input_type': 'yolo_dataset', ...}}
|
|
626
|
+
"""
|
|
627
|
+
import ast
|
|
628
|
+
import sys
|
|
629
|
+
|
|
630
|
+
from synapse_sdk.plugins.action import BaseAction
|
|
631
|
+
|
|
632
|
+
plugin_dir = Path(plugin_dir)
|
|
633
|
+
if not plugin_dir.is_dir():
|
|
634
|
+
raise ValueError(f'Plugin directory not found: {plugin_dir}')
|
|
635
|
+
|
|
636
|
+
discovered: dict[str, dict[str, Any]] = {}
|
|
637
|
+
|
|
638
|
+
# Add plugin directory to sys.path for imports
|
|
639
|
+
plugin_dir_str = str(plugin_dir)
|
|
640
|
+
path_added = False
|
|
641
|
+
if plugin_dir_str not in sys.path:
|
|
642
|
+
sys.path.insert(0, plugin_dir_str)
|
|
643
|
+
path_added = True
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
# Find all Python files (excluding __pycache__, tests, etc.)
|
|
647
|
+
py_files = list(plugin_dir.rglob('*.py'))
|
|
648
|
+
py_files = [
|
|
649
|
+
f
|
|
650
|
+
for f in py_files
|
|
651
|
+
if '__pycache__' not in str(f) and not f.name.startswith('test_') and f.name != 'conftest.py'
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
for py_file in py_files:
|
|
655
|
+
# Get module path relative to plugin directory
|
|
656
|
+
rel_path = py_file.relative_to(plugin_dir)
|
|
657
|
+
module_parts = list(rel_path.with_suffix('').parts)
|
|
658
|
+
|
|
659
|
+
# Skip __init__.py files for module path
|
|
660
|
+
if module_parts[-1] == '__init__':
|
|
661
|
+
module_parts = module_parts[:-1]
|
|
662
|
+
if not module_parts:
|
|
663
|
+
continue
|
|
664
|
+
|
|
665
|
+
module_name = '.'.join(module_parts)
|
|
666
|
+
|
|
667
|
+
# First, use AST to find potential BaseAction subclasses
|
|
668
|
+
# This avoids importing modules that would fail
|
|
669
|
+
try:
|
|
670
|
+
with py_file.open() as f:
|
|
671
|
+
tree = ast.parse(f.read(), filename=str(py_file))
|
|
672
|
+
except SyntaxError:
|
|
673
|
+
continue
|
|
674
|
+
|
|
675
|
+
# Look for class definitions that might be actions
|
|
676
|
+
potential_classes = []
|
|
677
|
+
for node in ast.walk(tree):
|
|
678
|
+
if isinstance(node, ast.ClassDef):
|
|
679
|
+
# Check if it has BaseAction or similar in bases
|
|
680
|
+
for base in node.bases:
|
|
681
|
+
base_name = ''
|
|
682
|
+
if isinstance(base, ast.Name):
|
|
683
|
+
base_name = base.id
|
|
684
|
+
elif isinstance(base, ast.Attribute):
|
|
685
|
+
base_name = base.attr
|
|
686
|
+
elif isinstance(base, ast.Subscript):
|
|
687
|
+
# Handle Generic[T] style: BaseAction[Params]
|
|
688
|
+
if isinstance(base.value, ast.Name):
|
|
689
|
+
base_name = base.value.id
|
|
690
|
+
elif isinstance(base.value, ast.Attribute):
|
|
691
|
+
base_name = base.value.attr
|
|
692
|
+
|
|
693
|
+
if 'Action' in base_name or 'Base' in base_name:
|
|
694
|
+
potential_classes.append(node.name)
|
|
695
|
+
break
|
|
696
|
+
|
|
697
|
+
if not potential_classes:
|
|
698
|
+
continue
|
|
699
|
+
|
|
700
|
+
# Now import the module and check actual inheritance
|
|
701
|
+
try:
|
|
702
|
+
module = importlib.import_module(module_name)
|
|
703
|
+
except Exception:
|
|
704
|
+
continue
|
|
705
|
+
|
|
706
|
+
for class_name in potential_classes:
|
|
707
|
+
try:
|
|
708
|
+
cls = getattr(module, class_name, None)
|
|
709
|
+
if cls is None:
|
|
710
|
+
continue
|
|
711
|
+
|
|
712
|
+
# Check if it's a proper BaseAction subclass
|
|
713
|
+
if not (
|
|
714
|
+
inspect.isclass(cls)
|
|
715
|
+
and issubclass(cls, BaseAction)
|
|
716
|
+
and cls is not BaseAction
|
|
717
|
+
and not inspect.isabstract(cls)
|
|
718
|
+
):
|
|
719
|
+
continue
|
|
720
|
+
|
|
721
|
+
# Extract action name
|
|
722
|
+
# Priority: action_name attr > derived from class name
|
|
723
|
+
action_name = getattr(cls, 'action_name', None)
|
|
724
|
+
if not action_name:
|
|
725
|
+
# Derive from class name: TrainAction -> train
|
|
726
|
+
action_name = class_name
|
|
727
|
+
if action_name.endswith('Action'):
|
|
728
|
+
action_name = action_name[:-6]
|
|
729
|
+
action_name = action_name.lower()
|
|
730
|
+
|
|
731
|
+
# Build entrypoint
|
|
732
|
+
entrypoint = f'{module_name}.{class_name}'
|
|
733
|
+
|
|
734
|
+
# Extract types
|
|
735
|
+
input_type = None
|
|
736
|
+
output_type = None
|
|
737
|
+
type_cls = getattr(cls, 'input_type', None)
|
|
738
|
+
if type_cls is not None and hasattr(type_cls, 'name'):
|
|
739
|
+
input_type = type_cls.name
|
|
740
|
+
type_cls = getattr(cls, 'output_type', None)
|
|
741
|
+
if type_cls is not None and hasattr(type_cls, 'name'):
|
|
742
|
+
output_type = type_cls.name
|
|
743
|
+
|
|
744
|
+
discovered[action_name] = {
|
|
745
|
+
'entrypoint': entrypoint,
|
|
746
|
+
'input_type': input_type,
|
|
747
|
+
'output_type': output_type,
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
except Exception:
|
|
751
|
+
continue
|
|
752
|
+
|
|
753
|
+
finally:
|
|
754
|
+
# Clean up sys.path
|
|
755
|
+
if path_added and plugin_dir_str in sys.path:
|
|
756
|
+
sys.path.remove(plugin_dir_str)
|
|
757
|
+
|
|
758
|
+
return discovered
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _load_entrypoint(entrypoint: str) -> type[BaseAction] | Callable:
|
|
762
|
+
"""Load class/function from entrypoint string.
|
|
763
|
+
|
|
764
|
+
Supports both colon and dot notation:
|
|
765
|
+
- Colon notation: 'module.path:ClassName' (preferred)
|
|
766
|
+
- Dot notation: 'module.path.ClassName' (common in config.yaml)
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
entrypoint: Entrypoint string like 'module.path:ClassName' or 'module.path.ClassName'
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
Loaded class or function
|
|
773
|
+
|
|
774
|
+
Raises:
|
|
775
|
+
ValueError: If entrypoint format is invalid
|
|
776
|
+
ModuleNotFoundError: If module doesn't exist
|
|
777
|
+
AttributeError: If attribute doesn't exist in module
|
|
778
|
+
"""
|
|
779
|
+
if ':' in entrypoint:
|
|
780
|
+
# Colon notation: 'module.path:ClassName'
|
|
781
|
+
module_path, attr_name = entrypoint.rsplit(':', 1)
|
|
782
|
+
else:
|
|
783
|
+
# Dot notation: 'module.path.ClassName' -> module='module.path', attr='ClassName'
|
|
784
|
+
module_path, attr_name = entrypoint.rsplit('.', 1)
|
|
785
|
+
|
|
786
|
+
module = importlib.import_module(module_path)
|
|
787
|
+
return getattr(module, attr_name)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
__all__ = ['PluginDiscovery']
|