silco 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- silco-0.1.0/PKG-INFO +78 -0
- silco-0.1.0/README.md +64 -0
- silco-0.1.0/pyproject.toml +39 -0
- silco-0.1.0/setup.cfg +4 -0
- silco-0.1.0/silco/__init__.py +15 -0
- silco-0.1.0/silco/core/__init__.py +31 -0
- silco-0.1.0/silco/core/kernel.py +209 -0
- silco-0.1.0/silco/core/models/__init__.py +12 -0
- silco-0.1.0/silco/core/models/edge.py +30 -0
- silco-0.1.0/silco/core/models/flow.py +6 -0
- silco-0.1.0/silco/core/models/group.py +24 -0
- silco-0.1.0/silco/core/models/node.py +42 -0
- silco-0.1.0/silco/core/renderers/__init__.py +35 -0
- silco-0.1.0/silco/core/renderers/base/__init__.py +0 -0
- silco-0.1.0/silco/core/renderers/base/config.py +20 -0
- silco-0.1.0/silco/core/renderers/base/diagram.py +179 -0
- silco-0.1.0/silco/core/renderers/base/graphics.py +88 -0
- silco-0.1.0/silco/core/renderers/base/layout.py +239 -0
- silco-0.1.0/silco/core/renderers/base/positioned_node.py +11 -0
- silco-0.1.0/silco/core/renderers/diagrams_backend.py +278 -0
- silco-0.1.0/silco/core/renderers/exporter/__init__.py +0 -0
- silco-0.1.0/silco/core/renderers/exporter/svg.py +254 -0
- silco-0.1.0/silco/core/renderers/style.py +3 -0
- silco-0.1.0/silco/core/renderers/svg_common.py +206 -0
- silco-0.1.0/silco/core/templates/shapes/actor.png +0 -0
- silco-0.1.0/silco/core/templates/shapes/actor.svg +9 -0
- silco-0.1.0/silco/core/templates/shapes/cache.svg +7 -0
- silco-0.1.0/silco/core/templates/shapes/component.svg +7 -0
- silco-0.1.0/silco/core/templates/shapes/database.svg +107 -0
- silco-0.1.0/silco/core/templates/shapes/external.svg +5 -0
- silco-0.1.0/silco/core/templates/shapes/group.svg +3 -0
- silco-0.1.0/silco/core/templates/shapes/queue.svg +5 -0
- silco-0.1.0/silco/core/templates/shapes/storage.svg +8 -0
- silco-0.1.0/silco/core/utils/__init__.py +0 -0
- silco-0.1.0/silco/plugins/__init__.py +8 -0
- silco-0.1.0/silco/plugins/ipython/__init__.py +60 -0
- silco-0.1.0/silco/plugins/pdf/__init__.py +54 -0
- silco-0.1.0/silco/plugins/renderers/__init__.py +10 -0
- silco-0.1.0/silco/plugins/renderers/mermaid.py +31 -0
- silco-0.1.0/silco/plugins/renderers/styles/__init__.py +5 -0
- silco-0.1.0/silco/plugins/renderers/styles/modern.py +59 -0
- silco-0.1.0/silco/plugins/renderers/styles/uml.py +59 -0
- silco-0.1.0/silco.egg-info/PKG-INFO +78 -0
- silco-0.1.0/silco.egg-info/SOURCES.txt +47 -0
- silco-0.1.0/silco.egg-info/dependency_links.txt +1 -0
- silco-0.1.0/silco.egg-info/entry_points.txt +3 -0
- silco-0.1.0/silco.egg-info/requires.txt +9 -0
- silco-0.1.0/silco.egg-info/top_level.txt +1 -0
- silco-0.1.0/tests/test_core.py +199 -0
silco-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: silco
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Microkernel-based Python toolkit for system diagrams, SVG rendering, and plugin-driven exports
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: diagrams>=0.25.1
|
|
8
|
+
Requires-Dist: graphviz>=0.20.3
|
|
9
|
+
Requires-Dist: pydantic>=2.13.3
|
|
10
|
+
Provides-Extra: notebook
|
|
11
|
+
Requires-Dist: ipython>=8; extra == "notebook"
|
|
12
|
+
Provides-Extra: pdf
|
|
13
|
+
Requires-Dist: cairosvg>=2.7; extra == "pdf"
|
|
14
|
+
|
|
15
|
+
# SILCO (**S**ystem **I**llustration & **L**ayout **Co**mposer)
|
|
16
|
+
|
|
17
|
+
Silco is a Python-first diagram toolkit built around a small microkernel. The core owns the document model, layout contracts, and plugin registry; renderers, styles, and presenters plug into that kernel. The default SVG renderer now uses the `diagrams` package with C4-flavored Graphviz output so the generated diagrams read like real system design documentation instead of custom-drawn boxes.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from silco import diagram
|
|
21
|
+
|
|
22
|
+
d = (
|
|
23
|
+
diagram("Checkout")
|
|
24
|
+
.node("user", "User", kind="actor")
|
|
25
|
+
.node("api", "API", kind="service", group="app")
|
|
26
|
+
.node("db", "Orders DB", kind="database", group="data")
|
|
27
|
+
.connect("user", "api", "HTTPS")
|
|
28
|
+
.connect("api", "db", "SQL")
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
svg = d.to_svg(style="modern")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Runtime requirement: Graphviz must be installed and `dot` must be on `PATH`.
|
|
35
|
+
|
|
36
|
+
Built-ins are registered through the kernel on import. Styles, layouts, and optional renderers stay discoverable:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from silco import kernel
|
|
40
|
+
|
|
41
|
+
kernel.names("layouts") # ("dag", "grid")
|
|
42
|
+
kernel.names("renderers") # ("svg", "mermaid", ...)
|
|
43
|
+
kernel.names("styles") # ("modern", "uml")
|
|
44
|
+
kernel.discover() # optional plugin discovery
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Render the same diagram in different ways:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
svg = d.to_svg(style="uml", layout="dag", title=True)
|
|
51
|
+
mermaid = d.to_mermaid()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
In IPython/Jupyter:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
%load_ext silco.plugins.ipython
|
|
58
|
+
d
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Export as PDF with the optional CairoSVG-backed renderer plugin:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install "silco[pdf]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
d.save_pdf("checkout.pdf")
|
|
69
|
+
pdf_bytes = d.to_pdf()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Plugin categories:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
kernel.categories()
|
|
76
|
+
kernel.names("renderers")
|
|
77
|
+
kernel.names("presenters")
|
|
78
|
+
```
|
silco-0.1.0/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# SILCO (**S**ystem **I**llustration & **L**ayout **Co**mposer)
|
|
2
|
+
|
|
3
|
+
Silco is a Python-first diagram toolkit built around a small microkernel. The core owns the document model, layout contracts, and plugin registry; renderers, styles, and presenters plug into that kernel. The default SVG renderer now uses the `diagrams` package with C4-flavored Graphviz output so the generated diagrams read like real system design documentation instead of custom-drawn boxes.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from silco import diagram
|
|
7
|
+
|
|
8
|
+
d = (
|
|
9
|
+
diagram("Checkout")
|
|
10
|
+
.node("user", "User", kind="actor")
|
|
11
|
+
.node("api", "API", kind="service", group="app")
|
|
12
|
+
.node("db", "Orders DB", kind="database", group="data")
|
|
13
|
+
.connect("user", "api", "HTTPS")
|
|
14
|
+
.connect("api", "db", "SQL")
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
svg = d.to_svg(style="modern")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Runtime requirement: Graphviz must be installed and `dot` must be on `PATH`.
|
|
21
|
+
|
|
22
|
+
Built-ins are registered through the kernel on import. Styles, layouts, and optional renderers stay discoverable:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from silco import kernel
|
|
26
|
+
|
|
27
|
+
kernel.names("layouts") # ("dag", "grid")
|
|
28
|
+
kernel.names("renderers") # ("svg", "mermaid", ...)
|
|
29
|
+
kernel.names("styles") # ("modern", "uml")
|
|
30
|
+
kernel.discover() # optional plugin discovery
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Render the same diagram in different ways:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
svg = d.to_svg(style="uml", layout="dag", title=True)
|
|
37
|
+
mermaid = d.to_mermaid()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
In IPython/Jupyter:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
%load_ext silco.plugins.ipython
|
|
44
|
+
d
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Export as PDF with the optional CairoSVG-backed renderer plugin:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install "silco[pdf]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
d.save_pdf("checkout.pdf")
|
|
55
|
+
pdf_bytes = d.to_pdf()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Plugin categories:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
kernel.categories()
|
|
62
|
+
kernel.names("renderers")
|
|
63
|
+
kernel.names("presenters")
|
|
64
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "silco"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Microkernel-based Python toolkit for system diagrams, SVG rendering, and plugin-driven exports"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"diagrams>=0.25.1",
|
|
13
|
+
"graphviz>=0.20.3",
|
|
14
|
+
"pydantic>=2.13.3",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
notebook = [
|
|
19
|
+
"ipython>=8",
|
|
20
|
+
]
|
|
21
|
+
pdf = [
|
|
22
|
+
"cairosvg>=2.7",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.entry-points."silco.plugins"]
|
|
26
|
+
ipython = "silco.plugins.ipython"
|
|
27
|
+
pdf = "silco.plugins.pdf"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
include-package-data = true
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
include = ["silco*"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
silco = [
|
|
37
|
+
"core/templates/shapes/*.svg",
|
|
38
|
+
"core/templates/shapes/*.png",
|
|
39
|
+
]
|
silco-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from silco.core import Canvas, Diagram, Edge, Element, Flow, Group, Node, PluginInfo, RenderConfig, diagram, kernel
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"Canvas",
|
|
5
|
+
"Diagram",
|
|
6
|
+
"Edge",
|
|
7
|
+
"Element",
|
|
8
|
+
"Flow",
|
|
9
|
+
"Group",
|
|
10
|
+
"Node",
|
|
11
|
+
"PluginInfo",
|
|
12
|
+
"RenderConfig",
|
|
13
|
+
"diagram",
|
|
14
|
+
"kernel",
|
|
15
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from silco.core.renderers.base.config import RenderConfig
|
|
2
|
+
from silco.core.renderers.base.diagram import Diagram, diagram
|
|
3
|
+
from silco.core.renderers.base.layout import Layout
|
|
4
|
+
from silco.core.renderers.base.positioned_node import PositionedNode
|
|
5
|
+
from silco.core.renderers.base.graphics import Canvas, Element
|
|
6
|
+
from silco.core.kernel import PLUGIN_CATEGORIES, PluginCategory, PluginInfo, SilcoKernel, kernel
|
|
7
|
+
from silco.core.models import Edge, Flow, Group, Node, NodeKind
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Canvas",
|
|
11
|
+
"Diagram",
|
|
12
|
+
"Edge",
|
|
13
|
+
"Element",
|
|
14
|
+
"Flow",
|
|
15
|
+
"Group",
|
|
16
|
+
"Layout",
|
|
17
|
+
"Node",
|
|
18
|
+
"NodeKind",
|
|
19
|
+
"PLUGIN_CATEGORIES",
|
|
20
|
+
"PluginCategory",
|
|
21
|
+
"PluginInfo",
|
|
22
|
+
"PositionedNode",
|
|
23
|
+
"RenderConfig",
|
|
24
|
+
"SilcoKernel",
|
|
25
|
+
"diagram",
|
|
26
|
+
"kernel",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Register built-in layouts/renderers on import.
|
|
30
|
+
# This keeps `Diagram.to_svg()` working out of the box.
|
|
31
|
+
from silco.core import renderers as _renderers # noqa: F401, E402
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import pkgutil
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from importlib import metadata
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
PluginCategory = Literal["shapes", "renderers", "layouts", "presenters", "styles"]
|
|
13
|
+
PluginType = PluginCategory
|
|
14
|
+
PLUGIN_CATEGORIES: tuple[PluginCategory, ...] = ("shapes", "renderers", "layouts", "presenters", "styles")
|
|
15
|
+
_CATEGORY_ALIASES = {
|
|
16
|
+
"shape": "shapes",
|
|
17
|
+
"shapes": "shapes",
|
|
18
|
+
"renderer": "renderers",
|
|
19
|
+
"renderers": "renderers",
|
|
20
|
+
"style": "styles",
|
|
21
|
+
"styles": "styles",
|
|
22
|
+
"layout": "layouts",
|
|
23
|
+
"layouts": "layouts",
|
|
24
|
+
"presenter": "presenters",
|
|
25
|
+
"presenters": "presenters",
|
|
26
|
+
"representation": "presenters",
|
|
27
|
+
"representations": "presenters",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PluginInfo:
|
|
33
|
+
"""Metadata describing a registered plugin."""
|
|
34
|
+
|
|
35
|
+
category: PluginCategory
|
|
36
|
+
name: str
|
|
37
|
+
plugin: Any
|
|
38
|
+
description: str | None = None
|
|
39
|
+
module: str | None = None
|
|
40
|
+
tags: tuple[str, ...] = field(default_factory=tuple)
|
|
41
|
+
auto_discovered: bool = False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SilcoKernel:
|
|
45
|
+
"""Registry and discovery hub for core and optional plugins."""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self.plugins: dict[PluginCategory, dict[str, Any]] = {category: {} for category in PLUGIN_CATEGORIES}
|
|
49
|
+
self._plugin_info: dict[PluginCategory, dict[str, PluginInfo]] = {category: {} for category in PLUGIN_CATEGORIES}
|
|
50
|
+
self._discovered_modules: set[str] = set()
|
|
51
|
+
self._discovered_entry_points: set[str] = set()
|
|
52
|
+
|
|
53
|
+
def register(
|
|
54
|
+
self,
|
|
55
|
+
plugin_type: str,
|
|
56
|
+
name: str,
|
|
57
|
+
plugin: Any,
|
|
58
|
+
*,
|
|
59
|
+
description: str | None = None,
|
|
60
|
+
module: str | None = None,
|
|
61
|
+
tags: Iterable[str] = (),
|
|
62
|
+
auto_discovered: bool = False,
|
|
63
|
+
) -> Any:
|
|
64
|
+
category = self.normalize_category(plugin_type)
|
|
65
|
+
if not name.strip():
|
|
66
|
+
raise ValueError("plugin name cannot be empty")
|
|
67
|
+
plugin_name = name.strip()
|
|
68
|
+
self.plugins[category][plugin_name] = plugin
|
|
69
|
+
self._plugin_info[category][plugin_name] = PluginInfo(
|
|
70
|
+
category=category,
|
|
71
|
+
name=plugin_name,
|
|
72
|
+
plugin=plugin,
|
|
73
|
+
description=description,
|
|
74
|
+
module=module or getattr(plugin, "__module__", None),
|
|
75
|
+
tags=tuple(tags),
|
|
76
|
+
auto_discovered=auto_discovered,
|
|
77
|
+
)
|
|
78
|
+
return plugin
|
|
79
|
+
|
|
80
|
+
def decorator(
|
|
81
|
+
self,
|
|
82
|
+
plugin_type: str,
|
|
83
|
+
name: str,
|
|
84
|
+
*,
|
|
85
|
+
description: str | None = None,
|
|
86
|
+
tags: Iterable[str] = (),
|
|
87
|
+
) -> Callable[[Any], Any]:
|
|
88
|
+
def register_plugin(plugin: Any) -> Any:
|
|
89
|
+
return self.register(plugin_type, name, plugin, description=description, tags=tags)
|
|
90
|
+
|
|
91
|
+
return register_plugin
|
|
92
|
+
|
|
93
|
+
def get(self, plugin_type: str, name: str, default: Any = None) -> Any:
|
|
94
|
+
category = self.normalize_category(plugin_type)
|
|
95
|
+
if default is None:
|
|
96
|
+
return self.plugins[category][name]
|
|
97
|
+
return self.plugins[category].get(name, default)
|
|
98
|
+
|
|
99
|
+
def info(self, plugin_type: str, name: str) -> PluginInfo:
|
|
100
|
+
category = self.normalize_category(plugin_type)
|
|
101
|
+
return self._plugin_info[category][name]
|
|
102
|
+
|
|
103
|
+
def list(self, plugin_type: str | None = None) -> dict[PluginCategory, tuple[PluginInfo, ...]] | tuple[PluginInfo, ...]:
|
|
104
|
+
if plugin_type is not None:
|
|
105
|
+
category = self.normalize_category(plugin_type)
|
|
106
|
+
return tuple(self._plugin_info[category].values())
|
|
107
|
+
return {category: tuple(self._plugin_info[category].values()) for category in PLUGIN_CATEGORIES}
|
|
108
|
+
|
|
109
|
+
def names(self, plugin_type: str) -> tuple[str, ...]:
|
|
110
|
+
category = self.normalize_category(plugin_type)
|
|
111
|
+
return tuple(self.plugins[category])
|
|
112
|
+
|
|
113
|
+
def categories(self) -> tuple[PluginCategory, ...]:
|
|
114
|
+
return PLUGIN_CATEGORIES
|
|
115
|
+
|
|
116
|
+
def normalize_category(self, plugin_type: str) -> PluginCategory:
|
|
117
|
+
try:
|
|
118
|
+
return _CATEGORY_ALIASES[plugin_type]
|
|
119
|
+
except KeyError as exc:
|
|
120
|
+
valid = ", ".join(PLUGIN_CATEGORIES)
|
|
121
|
+
raise ValueError(f"Unknown plugin category: {plugin_type}. Expected one of: {valid}") from exc
|
|
122
|
+
|
|
123
|
+
def discover(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
namespace: str = "silco.plugins",
|
|
127
|
+
entry_point_group: str = "silco.plugins",
|
|
128
|
+
) -> tuple[PluginInfo, ...]:
|
|
129
|
+
"""Import plugin modules and entry points, returning newly registered plugins."""
|
|
130
|
+
|
|
131
|
+
before = self._snapshot()
|
|
132
|
+
self.discover_namespace(namespace)
|
|
133
|
+
self.discover_entry_points(entry_point_group)
|
|
134
|
+
return self._new_plugins_since(before)
|
|
135
|
+
|
|
136
|
+
def discover_namespace(self, namespace: str = "silco.plugins") -> tuple[ModuleType, ...]:
|
|
137
|
+
package = importlib.import_module(namespace)
|
|
138
|
+
modules: list[ModuleType] = []
|
|
139
|
+
package_path = getattr(package, "__path__", None)
|
|
140
|
+
if package_path is None:
|
|
141
|
+
return tuple(modules)
|
|
142
|
+
for module_info in pkgutil.iter_modules(package_path, f"{namespace}."):
|
|
143
|
+
if module_info.name in self._discovered_modules:
|
|
144
|
+
continue
|
|
145
|
+
module = importlib.import_module(module_info.name)
|
|
146
|
+
self._discovered_modules.add(module_info.name)
|
|
147
|
+
self._register_from_module(module)
|
|
148
|
+
modules.append(module)
|
|
149
|
+
return tuple(modules)
|
|
150
|
+
|
|
151
|
+
def discover_entry_points(self, group: str = "silco.plugins") -> tuple[Any, ...]:
|
|
152
|
+
loaded: list[Any] = []
|
|
153
|
+
entry_points = metadata.entry_points()
|
|
154
|
+
if hasattr(entry_points, "select"):
|
|
155
|
+
selected = entry_points.select(group=group)
|
|
156
|
+
else: # pragma: no cover - compatibility with old importlib.metadata
|
|
157
|
+
selected = entry_points.get(group, ())
|
|
158
|
+
for entry_point in selected:
|
|
159
|
+
key = f"{entry_point.group}:{entry_point.name}"
|
|
160
|
+
if key in self._discovered_entry_points:
|
|
161
|
+
continue
|
|
162
|
+
plugin = entry_point.load()
|
|
163
|
+
self._discovered_entry_points.add(key)
|
|
164
|
+
self._register_loaded_plugin(entry_point.name, plugin)
|
|
165
|
+
loaded.append(plugin)
|
|
166
|
+
return tuple(loaded)
|
|
167
|
+
|
|
168
|
+
def _register_from_module(self, module: ModuleType) -> None:
|
|
169
|
+
register = getattr(module, "register_plugins", None)
|
|
170
|
+
if callable(register):
|
|
171
|
+
register(self)
|
|
172
|
+
|
|
173
|
+
def _register_loaded_plugin(self, name: str, plugin: Any) -> None:
|
|
174
|
+
if inspect.ismodule(plugin):
|
|
175
|
+
self._register_from_module(plugin)
|
|
176
|
+
return
|
|
177
|
+
register = getattr(plugin, "register_plugins", None)
|
|
178
|
+
if callable(register):
|
|
179
|
+
register(self)
|
|
180
|
+
return
|
|
181
|
+
category = getattr(plugin, "silco_plugin_category", None)
|
|
182
|
+
plugin_name = getattr(plugin, "silco_plugin_name", name)
|
|
183
|
+
if category is None and callable(plugin):
|
|
184
|
+
plugin(self)
|
|
185
|
+
return
|
|
186
|
+
if category is None:
|
|
187
|
+
raise ValueError(f"Discovered plugin {name!r} does not declare a Silco category")
|
|
188
|
+
self.register(
|
|
189
|
+
category,
|
|
190
|
+
plugin_name,
|
|
191
|
+
plugin,
|
|
192
|
+
description=getattr(plugin, "silco_plugin_description", None),
|
|
193
|
+
tags=getattr(plugin, "silco_plugin_tags", ()),
|
|
194
|
+
auto_discovered=True,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
def _snapshot(self) -> set[tuple[PluginCategory, str]]:
|
|
198
|
+
return {(category, name) for category, plugins in self.plugins.items() for name in plugins}
|
|
199
|
+
|
|
200
|
+
def _new_plugins_since(self, snapshot: set[tuple[PluginCategory, str]]) -> tuple[PluginInfo, ...]:
|
|
201
|
+
new_plugins: list[PluginInfo] = []
|
|
202
|
+
for category, infos in self._plugin_info.items():
|
|
203
|
+
for name, info in infos.items():
|
|
204
|
+
if (category, name) not in snapshot:
|
|
205
|
+
new_plugins.append(info)
|
|
206
|
+
return tuple(new_plugins)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
kernel = SilcoKernel()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Edge(BaseModel):
|
|
6
|
+
"""A directed relationship between two nodes."""
|
|
7
|
+
|
|
8
|
+
model_config = ConfigDict(extra="forbid")
|
|
9
|
+
|
|
10
|
+
source: str = Field(min_length=1)
|
|
11
|
+
target: str = Field(min_length=1)
|
|
12
|
+
label: str | None = None
|
|
13
|
+
protocol: str | None = None
|
|
14
|
+
bidirectional: bool = False
|
|
15
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
@field_validator("source", "target")
|
|
18
|
+
@classmethod
|
|
19
|
+
def normalize_endpoint(cls, value: str) -> str:
|
|
20
|
+
value = value.strip()
|
|
21
|
+
if not value:
|
|
22
|
+
raise ValueError("edge endpoint cannot be empty")
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def display_label(self) -> str | None:
|
|
27
|
+
if self.label and self.protocol:
|
|
28
|
+
return f"{self.label} ({self.protocol})"
|
|
29
|
+
return self.label or self.protocol
|
|
30
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Group(BaseModel):
|
|
6
|
+
"""A visual boundary for a set of related nodes."""
|
|
7
|
+
|
|
8
|
+
model_config = ConfigDict(extra="forbid")
|
|
9
|
+
|
|
10
|
+
id: str = Field(min_length=1)
|
|
11
|
+
label: str | None = None
|
|
12
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
@field_validator("id")
|
|
15
|
+
@classmethod
|
|
16
|
+
def normalize_identifier(cls, value: str) -> str:
|
|
17
|
+
value = value.strip()
|
|
18
|
+
if not value:
|
|
19
|
+
raise ValueError("group id cannot be empty")
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def display_label(self) -> str:
|
|
24
|
+
return self.label or self.id
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
4
|
+
|
|
5
|
+
NodeKind = Literal[
|
|
6
|
+
"actor",
|
|
7
|
+
"service",
|
|
8
|
+
"database",
|
|
9
|
+
"queue",
|
|
10
|
+
"cache",
|
|
11
|
+
"storage",
|
|
12
|
+
"external",
|
|
13
|
+
"component",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Node(BaseModel):
|
|
18
|
+
"""A system component that can be rendered in a diagram."""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(extra="forbid")
|
|
21
|
+
|
|
22
|
+
id: str = Field(min_length=1)
|
|
23
|
+
label: str | None = None
|
|
24
|
+
kind: NodeKind = "component"
|
|
25
|
+
description: str | None = None
|
|
26
|
+
group: str | None = None
|
|
27
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
@field_validator("id", "group")
|
|
30
|
+
@classmethod
|
|
31
|
+
def normalize_identifier(cls, value: str | None) -> str | None:
|
|
32
|
+
if value is None:
|
|
33
|
+
return None
|
|
34
|
+
value = value.strip()
|
|
35
|
+
if not value:
|
|
36
|
+
raise ValueError("identifier cannot be empty")
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def display_label(self) -> str:
|
|
41
|
+
return self.label or self.id
|
|
42
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from silco.core.renderers.exporter.svg import svg_renderer
|
|
2
|
+
from silco.core.kernel import kernel
|
|
3
|
+
from silco.core.renderers.base.layout import dag_layout, grid_layout
|
|
4
|
+
from silco.core.renderers.svg_common import render_mermaid
|
|
5
|
+
|
|
6
|
+
kernel.register(
|
|
7
|
+
"layouts",
|
|
8
|
+
"dag",
|
|
9
|
+
dag_layout,
|
|
10
|
+
description="Layered directed-acyclic-graph layout for left-to-right system flows.",
|
|
11
|
+
tags=("builtin", "layout"),
|
|
12
|
+
)
|
|
13
|
+
kernel.register(
|
|
14
|
+
"layouts",
|
|
15
|
+
"grid",
|
|
16
|
+
grid_layout,
|
|
17
|
+
description="Simple grid layout for unordered component maps.",
|
|
18
|
+
tags=("builtin", "layout"),
|
|
19
|
+
)
|
|
20
|
+
kernel.register(
|
|
21
|
+
"renderers",
|
|
22
|
+
"svg",
|
|
23
|
+
svg_renderer,
|
|
24
|
+
description="Standalone SVG renderer for notebooks, docs, and exports.",
|
|
25
|
+
tags=("builtin", "renderer", "vector"),
|
|
26
|
+
)
|
|
27
|
+
kernel.register(
|
|
28
|
+
"renderers",
|
|
29
|
+
"mermaid",
|
|
30
|
+
render_mermaid,
|
|
31
|
+
description="Mermaid flowchart text renderer.",
|
|
32
|
+
tags=("builtin", "renderer", "text"),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = ["render_mermaid", "svg_renderer"]
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RenderConfig(BaseModel):
|
|
7
|
+
"""Rendering knobs shared by built-in renderers."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
direction: str = "LR"
|
|
12
|
+
width: int = Field(default=960, ge=320)
|
|
13
|
+
node_width: int = Field(default=168, ge=80)
|
|
14
|
+
node_height: int = Field(default=84, ge=48)
|
|
15
|
+
rank_gap: int = Field(default=72, ge=40)
|
|
16
|
+
node_gap: int = Field(default=48, ge=20)
|
|
17
|
+
margin: int = Field(default=32, ge=0)
|
|
18
|
+
style: str = Field(default="modern", min_length=1)
|
|
19
|
+
font_family: str = "ui-sans-serif, Segoe UI, sans-serif"
|
|
20
|
+
title: bool = True
|