katalyst-plugin-scaffolding 1.0.1a1__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.
- katalyst_plugin_scaffolding/__init__.py +5 -0
- katalyst_plugin_scaffolding/linkml/__init__.py +15 -0
- katalyst_plugin_scaffolding/linkml/released/0.1.0/SHA256SUMS +1 -0
- katalyst_plugin_scaffolding/linkml/released/0.1.0/scaffolding.yaml +54 -0
- katalyst_plugin_scaffolding/linkml/scaffolding.yaml +54 -0
- katalyst_plugin_scaffolding/plugin.py +79 -0
- katalyst_plugin_scaffolding/py.typed +0 -0
- katalyst_plugin_scaffolding/rest_routes.py +117 -0
- katalyst_plugin_scaffolding/schema.py +75 -0
- katalyst_plugin_scaffolding/templates/__init__.py +6 -0
- katalyst_plugin_scaffolding/templates/bundle_install.py +238 -0
- katalyst_plugin_scaffolding/templates/cli.py +177 -0
- katalyst_plugin_scaffolding/templates/composed_install.py +360 -0
- katalyst_plugin_scaffolding/templates/layout.py +332 -0
- katalyst_plugin_scaffolding/templates/plugin.py +142 -0
- katalyst_plugin_scaffolding/templates/plugin_dirs.py +44 -0
- katalyst_plugin_scaffolding/templates/py.typed +0 -0
- katalyst_plugin_scaffolding/templates/recipe_wiring.py +120 -0
- katalyst_plugin_scaffolding/templates/template_remove.py +153 -0
- katalyst_plugin_scaffolding-1.0.1a1.dist-info/METADATA +16 -0
- katalyst_plugin_scaffolding-1.0.1a1.dist-info/RECORD +23 -0
- katalyst_plugin_scaffolding-1.0.1a1.dist-info/WHEEL +4 -0
- katalyst_plugin_scaffolding-1.0.1a1.dist-info/entry_points.txt +11 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""LinkML modules this package ships, on katalyst-core.
|
|
2
|
+
|
|
3
|
+
Registered under the ``katalyst_core.modules`` entry point, so core loads them by name.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
__all__ = ["MODULES"]
|
|
11
|
+
|
|
12
|
+
MODULES: dict[str, Path] = {
|
|
13
|
+
path.stem: path for path in sorted(Path(__file__).parent.glob("*.yaml"))
|
|
14
|
+
}
|
|
15
|
+
"""Module name (as an ``imports:`` line writes it) to its YAML file."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cb3f7942331947e4995dc5a067f51435bd59e2ea7292eabdf1be26eb2141340b scaffolding.yaml
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Generated once from the Python definitions (LinkML rebuild, phase 4); edit here.
|
|
2
|
+
id: https://w3id.org/katalyst/scaffolding/scaffolding
|
|
3
|
+
name: scaffolding
|
|
4
|
+
title: scaffolding scaffolding wing
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
license: Proprietary
|
|
7
|
+
prefixes:
|
|
8
|
+
scaffolding: https://w3id.org/katalyst/scaffolding/
|
|
9
|
+
kc: https://w3id.org/katalyst/core/
|
|
10
|
+
linkml: https://w3id.org/linkml/
|
|
11
|
+
dcterms: http://purl.org/dc/terms/
|
|
12
|
+
default_prefix: scaffolding
|
|
13
|
+
default_range: string
|
|
14
|
+
imports:
|
|
15
|
+
- linkml:types
|
|
16
|
+
- katalyst_core
|
|
17
|
+
description: The scaffolding wing.
|
|
18
|
+
annotations:
|
|
19
|
+
wing: scaffolding
|
|
20
|
+
display_name: Scaffolding
|
|
21
|
+
describes_kind: wing
|
|
22
|
+
classes:
|
|
23
|
+
NodeWiring:
|
|
24
|
+
is_a: Node
|
|
25
|
+
class_uri: scaffolding:NodeWiring
|
|
26
|
+
description: 'A node wiring: a node_wiring node of the scaffolding wing.'
|
|
27
|
+
attributes:
|
|
28
|
+
description:
|
|
29
|
+
description: What this wiring adds to scaffolded nodes
|
|
30
|
+
slot_uri: dcterms:description
|
|
31
|
+
extends:
|
|
32
|
+
description: 'The templates it extends: targetKind and targetNames (''*'' for all)'
|
|
33
|
+
slot_uri: scaffolding:extends
|
|
34
|
+
range: AnyValue
|
|
35
|
+
provides_template:
|
|
36
|
+
description: Template directory whose files are added, relative to this document
|
|
37
|
+
slot_uri: scaffolding:providesTemplate
|
|
38
|
+
annotations:
|
|
39
|
+
kind: node_wiring
|
|
40
|
+
wing: scaffolding
|
|
41
|
+
label: Node Wiring
|
|
42
|
+
label_plural: Node Wirings
|
|
43
|
+
display_order: 900
|
|
44
|
+
directory_name: node_wiring
|
|
45
|
+
root: true
|
|
46
|
+
leaf: true
|
|
47
|
+
storage:
|
|
48
|
+
tag: storage
|
|
49
|
+
value:
|
|
50
|
+
root: .global/taxonomy
|
|
51
|
+
container: file
|
|
52
|
+
filename: identity
|
|
53
|
+
nest_under_parent: false
|
|
54
|
+
scaffold: null
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Generated once from the Python definitions (LinkML rebuild, phase 4); edit here.
|
|
2
|
+
id: https://w3id.org/katalyst/scaffolding/scaffolding
|
|
3
|
+
name: scaffolding
|
|
4
|
+
title: scaffolding scaffolding wing
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
license: Proprietary
|
|
7
|
+
prefixes:
|
|
8
|
+
scaffolding: https://w3id.org/katalyst/scaffolding/
|
|
9
|
+
kc: https://w3id.org/katalyst/core/
|
|
10
|
+
linkml: https://w3id.org/linkml/
|
|
11
|
+
dcterms: http://purl.org/dc/terms/
|
|
12
|
+
default_prefix: scaffolding
|
|
13
|
+
default_range: string
|
|
14
|
+
imports:
|
|
15
|
+
- linkml:types
|
|
16
|
+
- katalyst_core
|
|
17
|
+
description: The scaffolding wing.
|
|
18
|
+
annotations:
|
|
19
|
+
wing: scaffolding
|
|
20
|
+
display_name: Scaffolding
|
|
21
|
+
describes_kind: wing
|
|
22
|
+
classes:
|
|
23
|
+
NodeWiring:
|
|
24
|
+
is_a: Node
|
|
25
|
+
class_uri: scaffolding:NodeWiring
|
|
26
|
+
description: 'A node wiring: a node_wiring node of the scaffolding wing.'
|
|
27
|
+
attributes:
|
|
28
|
+
description:
|
|
29
|
+
description: What this wiring adds to scaffolded nodes
|
|
30
|
+
slot_uri: dcterms:description
|
|
31
|
+
extends:
|
|
32
|
+
description: 'The templates it extends: targetKind and targetNames (''*'' for all)'
|
|
33
|
+
slot_uri: scaffolding:extends
|
|
34
|
+
range: AnyValue
|
|
35
|
+
provides_template:
|
|
36
|
+
description: Template directory whose files are added, relative to this document
|
|
37
|
+
slot_uri: scaffolding:providesTemplate
|
|
38
|
+
annotations:
|
|
39
|
+
kind: node_wiring
|
|
40
|
+
wing: scaffolding
|
|
41
|
+
label: Node Wiring
|
|
42
|
+
label_plural: Node Wirings
|
|
43
|
+
display_order: 900
|
|
44
|
+
directory_name: node_wiring
|
|
45
|
+
root: true
|
|
46
|
+
leaf: true
|
|
47
|
+
storage:
|
|
48
|
+
tag: storage
|
|
49
|
+
value:
|
|
50
|
+
root: .global/taxonomy
|
|
51
|
+
container: file
|
|
52
|
+
filename: identity
|
|
53
|
+
nest_under_parent: false
|
|
54
|
+
scaffold: null
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Scaffolding plugin — the template-bundle machinery scaffolding copies from.
|
|
2
|
+
|
|
3
|
+
Registers how template bundles are installed into, removed from, listed in and described
|
|
4
|
+
for a repository; how a fetched collection's bundle manifests are found; the probe
|
|
5
|
+
``node_wiring`` documents check; the create hook that writes a scaffolded node's
|
|
6
|
+
wiring file; and ``/ext/scaffolding/variants``, the installed variants a form offers
|
|
7
|
+
(:mod:`katalyst_plugin_scaffolding.rest_routes`). ``kata tax add`` arrives over the ``katalyst_taxonomy.cli`` entry point.
|
|
8
|
+
|
|
9
|
+
The scaffold copy itself is not here. The runtime's scaffolding hook
|
|
10
|
+
(:mod:`katalyst_taxonomy.services.scaffolding_hook`) populates a new node's directory
|
|
11
|
+
when a scaffold variant resolves and the kind's template root holds a directory for it
|
|
12
|
+
(otherwise it is a no-op); the variants it copies are the templates this plugin
|
|
13
|
+
installed. There is no engine-level ``scaffoldable`` flag: where a node records its
|
|
14
|
+
variant is its kind's declaration (``ScaffoldPolicy.variant_annotation``, the field with
|
|
15
|
+
the scaffold-variant role).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import importlib.metadata
|
|
21
|
+
|
|
22
|
+
from katalyst_taxonomy.plugins.protocol import PluginContext, PluginManifest
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _version() -> str:
|
|
26
|
+
"""The installed distribution version, for :attr:`PluginManifest.version`.
|
|
27
|
+
|
|
28
|
+
No built-in plugin populated ``version`` before #219 — it was declared and
|
|
29
|
+
always empty, so ``kata config plugins`` could not tell you what you were
|
|
30
|
+
running. Read from installed metadata rather than a hardcoded literal so it
|
|
31
|
+
cannot drift from the package it describes.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
return importlib.metadata.version("katalyst-plugin-scaffolding")
|
|
35
|
+
except importlib.metadata.PackageNotFoundError: # pragma: no cover - source checkout
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ScaffoldingPlugin:
|
|
40
|
+
"""The scaffolding plugin: template-bundle machinery and the node wiring create hook.
|
|
41
|
+
|
|
42
|
+
``scaffold``: it installs the templates scaffolding copies; ``post_create``: its create
|
|
43
|
+
hook runs after a node is created.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def name(self) -> str:
|
|
48
|
+
return "scaffolding"
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def manifest(self) -> PluginManifest:
|
|
52
|
+
return PluginManifest(
|
|
53
|
+
name="scaffolding",
|
|
54
|
+
capabilities=("scaffold", "post_create"),
|
|
55
|
+
description="Template bundles for scaffolding, and the node wiring create hook",
|
|
56
|
+
version=_version(),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def register(self, context: PluginContext) -> None:
|
|
60
|
+
"""Register the template-bundle machinery and the create hook with *context*."""
|
|
61
|
+
from katalyst_plugin_scaffolding.rest_routes import ScaffoldingRestRoutes
|
|
62
|
+
from katalyst_plugin_scaffolding.templates.bundle_install import (
|
|
63
|
+
describe_bundle,
|
|
64
|
+
install_template_for_plugin,
|
|
65
|
+
installed_bundles,
|
|
66
|
+
)
|
|
67
|
+
from katalyst_plugin_scaffolding.templates.plugin import register_template_contributions
|
|
68
|
+
from katalyst_plugin_scaffolding.templates.template_remove import (
|
|
69
|
+
remove_template_for_plugin,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# No init hook: `kata tax init` installs what the user config declares
|
|
73
|
+
# (spec.templates), which a bundle's own init hook may add to.
|
|
74
|
+
register_template_contributions(context)
|
|
75
|
+
context.rest_routes.append(ScaffoldingRestRoutes())
|
|
76
|
+
context.template_installers.append(install_template_for_plugin)
|
|
77
|
+
context.template_removers.append(remove_template_for_plugin)
|
|
78
|
+
context.template_listers.append(installed_bundles)
|
|
79
|
+
context.template_describers.append(describe_bundle)
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# pyright: reportUnusedFunction=false
|
|
2
|
+
#
|
|
3
|
+
# The routes are nested functions registered by their decorators and never referenced by
|
|
4
|
+
# name; the REST app mounts the router this returns.
|
|
5
|
+
"""``/ext/scaffolding`` — the installed scaffold variants, for a form that creates nodes.
|
|
6
|
+
|
|
7
|
+
================================== ======================================================
|
|
8
|
+
Route Answers
|
|
9
|
+
================================== ======================================================
|
|
10
|
+
``GET /ext/scaffolding/variants`` every scaffold variant installed in the repository
|
|
11
|
+
(``?kind=`` one kind's; ``?holds=ROLE`` those whose
|
|
12
|
+
nodes hold ROLE): its bundle and version, its
|
|
13
|
+
declaration, what its nodes hold, its files, its
|
|
14
|
+
expansions and every token it renders with where the
|
|
15
|
+
value comes from
|
|
16
|
+
================================== ======================================================
|
|
17
|
+
|
|
18
|
+
A read: nothing is written. ``backend`` names the repository, as every REST route that
|
|
19
|
+
reads one repository's files does. The answer is
|
|
20
|
+
:mod:`katalyst_taxonomy.services.scaffold_catalog`'s.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# No `from __future__ import annotations`: FastAPI reads the handlers' annotations.
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ScaffoldingRestRoutes:
|
|
29
|
+
"""Implements :class:`~katalyst_taxonomy.plugins.extensions.RestRoutes`."""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
return "scaffolding"
|
|
34
|
+
|
|
35
|
+
def router(self) -> Any:
|
|
36
|
+
from fastapi import APIRouter
|
|
37
|
+
from fastapi.responses import JSONResponse
|
|
38
|
+
from katalyst_taxonomy.config.installs import declared_config
|
|
39
|
+
from katalyst_taxonomy.services.scaffold_catalog import (
|
|
40
|
+
BundleFiles,
|
|
41
|
+
declared_tokens,
|
|
42
|
+
scaffold_variants,
|
|
43
|
+
)
|
|
44
|
+
from katalyst_taxonomy_mcp.response import ok
|
|
45
|
+
from katalyst_taxonomy_mcp.rest.repository import repository_or_refusal
|
|
46
|
+
|
|
47
|
+
from katalyst_plugin_scaffolding.templates.bundle_install import (
|
|
48
|
+
describe_bundle,
|
|
49
|
+
installed_bundles,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
router = APIRouter()
|
|
53
|
+
|
|
54
|
+
def bundles(root: Path) -> list[BundleFiles]:
|
|
55
|
+
"""The declared template bundles *root* has installed, with the files of each."""
|
|
56
|
+
config = declared_config()[0]
|
|
57
|
+
declared = config.spec.templates if config is not None else ()
|
|
58
|
+
have = set(installed_bundles(root, {t.name: t.source for t in declared}))
|
|
59
|
+
out: list[BundleFiles] = []
|
|
60
|
+
for template in declared:
|
|
61
|
+
if template.name not in have:
|
|
62
|
+
continue
|
|
63
|
+
described = describe_bundle(root, template.name, template.source) or {}
|
|
64
|
+
files = [str(f) for f in described.get("files") or ()]
|
|
65
|
+
out.append(BundleFiles(template.name, str(template.version or ""), files))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
@router.get("/variants", response_model=None)
|
|
69
|
+
def variants(
|
|
70
|
+
kind: str | None = None, holds: str | None = None, backend: str | None = None
|
|
71
|
+
) -> Any:
|
|
72
|
+
"""The variants installed in *backend*'s repository: of *kind*, holding *holds*."""
|
|
73
|
+
repository = repository_or_refusal(backend)
|
|
74
|
+
if isinstance(repository, JSONResponse):
|
|
75
|
+
return repository
|
|
76
|
+
root = repository.root
|
|
77
|
+
found = scaffold_variants(
|
|
78
|
+
root,
|
|
79
|
+
filesystem=_Files(),
|
|
80
|
+
bundles=bundles(root),
|
|
81
|
+
kind=kind or None,
|
|
82
|
+
holds=holds or None,
|
|
83
|
+
)
|
|
84
|
+
return ok(
|
|
85
|
+
{
|
|
86
|
+
"backend": repository.name,
|
|
87
|
+
"kind": kind,
|
|
88
|
+
"holds": holds,
|
|
89
|
+
"variants": [variant.to_dict() for variant in found],
|
|
90
|
+
# The kind's own scaffold tokens: fields only a template renders.
|
|
91
|
+
"tokens": declared_tokens(kind) if kind else [],
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return router
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class _Files:
|
|
99
|
+
"""The repository's files, read — the ``FileReader`` the catalog reads through."""
|
|
100
|
+
|
|
101
|
+
def exists(self, path: Path) -> bool:
|
|
102
|
+
return path.exists()
|
|
103
|
+
|
|
104
|
+
def is_directory(self, path: Path) -> bool:
|
|
105
|
+
return path.is_dir()
|
|
106
|
+
|
|
107
|
+
def is_file(self, path: Path) -> bool:
|
|
108
|
+
return path.is_file()
|
|
109
|
+
|
|
110
|
+
def read_file(self, path: Path) -> str:
|
|
111
|
+
return path.read_text(encoding="utf-8")
|
|
112
|
+
|
|
113
|
+
def list_directory(self, path: Path) -> list[Path]:
|
|
114
|
+
return list(path.iterdir()) if path.is_dir() else []
|
|
115
|
+
|
|
116
|
+
def glob(self, path: Path, pattern: str) -> list[Path]:
|
|
117
|
+
return list(path.glob(pattern))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Schema bundle for the scaffolding plugin: the ``node_wiring`` kind.
|
|
2
|
+
|
|
3
|
+
A ``node_wiring`` document is ordinary taxonomy content. It lives in a repository,
|
|
4
|
+
under ``.global/taxonomy/node_wiring/``, and adds files to scaffolded nodes: it names
|
|
5
|
+
the templates it extends and the template directory it provides, and it activates only
|
|
6
|
+
when the plugins it requires are installed.
|
|
7
|
+
|
|
8
|
+
Declaring it (in the LinkML module ``linkml/scaffolding.yaml``) makes it a registered kind like any other, validated and listed. The
|
|
9
|
+
runtime keeps no list of kinds to skip.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from functools import cache
|
|
15
|
+
|
|
16
|
+
from katalyst_engine.bundle.protocol import BundleManifest
|
|
17
|
+
from katalyst_engine.core.identity import Identity
|
|
18
|
+
from katalyst_engine.core.version import Version
|
|
19
|
+
from katalyst_engine.schema.definition import (
|
|
20
|
+
NodeDefinition,
|
|
21
|
+
SchemaDefinition,
|
|
22
|
+
WingDefinition,
|
|
23
|
+
)
|
|
24
|
+
from katalyst_engine.schema.manager import SchemaManager
|
|
25
|
+
from katalyst_engine.toolkit.linkml import LinkMLDefinitions, installed_definitions
|
|
26
|
+
|
|
27
|
+
#: The bundle's entry-point name, as a profile's ``schemas`` list names it.
|
|
28
|
+
BUNDLE_NAME = "scaffolding-schema"
|
|
29
|
+
WING = "scaffolding"
|
|
30
|
+
KIND = "node_wiring"
|
|
31
|
+
|
|
32
|
+
#: The LinkML module this bundle builds its definitions from (``linkml/scaffolding.yaml``).
|
|
33
|
+
MODULES = ("scaffolding",)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cache
|
|
37
|
+
def _definitions() -> LinkMLDefinitions:
|
|
38
|
+
return installed_definitions(MODULES)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
NODE_WIRING: NodeDefinition = next(n for n in _definitions().nodes if n.describes_kind == KIND)
|
|
42
|
+
SCAFFOLDING_WING: WingDefinition = next(w for w in _definitions().wings if w.identity.name == WING)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ScaffoldingSchemaBundle:
|
|
46
|
+
"""Contributes ``node_wiring``, and the wing it lives in."""
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def manifest(self) -> BundleManifest:
|
|
50
|
+
return BundleManifest(
|
|
51
|
+
identity=Identity(kind="bundle", name=BUNDLE_NAME),
|
|
52
|
+
version=Version(major=0, minor=1),
|
|
53
|
+
modules=MODULES,
|
|
54
|
+
provided_kinds=frozenset({KIND}),
|
|
55
|
+
wing_names=frozenset({WING}),
|
|
56
|
+
description="Schema for the documents the scaffolding plugin reads",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def node_definitions(self) -> list[NodeDefinition]:
|
|
60
|
+
return [NODE_WIRING]
|
|
61
|
+
|
|
62
|
+
def wing_definitions(self) -> list[WingDefinition]:
|
|
63
|
+
return [SCAFFOLDING_WING]
|
|
64
|
+
|
|
65
|
+
def schema_definition(self) -> SchemaDefinition:
|
|
66
|
+
return SchemaDefinition(
|
|
67
|
+
identity=Identity(kind="schema", name=BUNDLE_NAME),
|
|
68
|
+
describes_kind="taxonomy",
|
|
69
|
+
api_version="v2alpha",
|
|
70
|
+
schema_version=Version(major=0, minor=1),
|
|
71
|
+
node_definitions=(NODE_WIRING.identity,),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def register(self, manager: SchemaManager) -> None:
|
|
75
|
+
manager.register_schema(self.schema_definition(), node_definitions=self.node_definitions())
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Template bundles, as a taxonomy plugin.
|
|
2
|
+
|
|
3
|
+
Installs, lists, describes and removes the template bundles a user config declares
|
|
4
|
+
(``spec.templates``) — every bundle the same way, from its manifest in its declared
|
|
5
|
+
source — and contributes the ``kata tax add`` command.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Install forge template bundles into a taxonomy repository (the `kata tax add` engine).
|
|
2
|
+
|
|
3
|
+
A collection declares its layout in ``templates/manifest.json`` (``layout:
|
|
4
|
+
wing/kind/template``): a bundle is ``templates/_manifests/<name>.yaml`` listing the
|
|
5
|
+
templates it is made of, each of which is installed where its kind declares (see
|
|
6
|
+
:mod:`~katalyst_plugin_scaffolding.templates.composed_install`). The collection sits at
|
|
7
|
+
the root of a git or local source, or one or two package levels down in a wheel.
|
|
8
|
+
|
|
9
|
+
Every bundle is handled the same way, whatever kinds its templates are:
|
|
10
|
+
|
|
11
|
+
* installed by copying its templates (:func:`install_template_bundle`);
|
|
12
|
+
* installed *when* every path that copy writes is on disk (:func:`bundle_installed`);
|
|
13
|
+
* described by those paths and its templates' kinds (:func:`describe_bundle`);
|
|
14
|
+
* removed by deleting those paths (:mod:`~katalyst_plugin_scaffolding.templates.template_remove`).
|
|
15
|
+
|
|
16
|
+
Each reads the bundle's manifest in its declared source (a ``spec.templates`` entry). It
|
|
17
|
+
copies; it records nothing — the pinned source and version go on the template's
|
|
18
|
+
``spec.templates`` entry in ``config.yaml`` (:mod:`katalyst_taxonomy.services.template_install`).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import subprocess
|
|
24
|
+
from collections.abc import Mapping
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, cast
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
from katalyst_taxonomy.sources.bundle_sources import fetch_source_tree
|
|
30
|
+
|
|
31
|
+
from katalyst_plugin_scaffolding.templates.composed_install import (
|
|
32
|
+
BundleInstallResult,
|
|
33
|
+
bundle_targets,
|
|
34
|
+
composed_bundle_installed,
|
|
35
|
+
install_composed_bundle,
|
|
36
|
+
is_composed_manifest,
|
|
37
|
+
load_manifest,
|
|
38
|
+
)
|
|
39
|
+
from katalyst_plugin_scaffolding.templates.layout import LayoutSpec, TemplateRef, read_layout_spec
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def collection_root(checkout_root: Path) -> Path:
|
|
43
|
+
"""The directory holding ``templates/``: the root, or a package one or two levels down.
|
|
44
|
+
|
|
45
|
+
A git or local source has it at the root. A wheel's top level is the installed
|
|
46
|
+
package(s) plus ``*.dist-info`` / ``*.data``, so the collection sits at
|
|
47
|
+
``<package>/templates/`` or ``<package>/<subdir>/templates/`` (for instance
|
|
48
|
+
``<package>/collection/templates/``). Below the root, a ``templates/`` that declares
|
|
49
|
+
a collection (``templates/manifest.json`` or ``templates/_manifests/``) wins over one
|
|
50
|
+
that does not: a package may have a ``templates`` subpackage of its own beside its
|
|
51
|
+
collection. Falls back to *checkout_root* so the caller reports "bundle not found"
|
|
52
|
+
rather than an internal error.
|
|
53
|
+
"""
|
|
54
|
+
if (checkout_root / "templates").is_dir():
|
|
55
|
+
return checkout_root
|
|
56
|
+
if not checkout_root.is_dir():
|
|
57
|
+
return checkout_root
|
|
58
|
+
level = [
|
|
59
|
+
c
|
|
60
|
+
for c in sorted(checkout_root.iterdir())
|
|
61
|
+
if c.is_dir()
|
|
62
|
+
and not c.name.endswith((".dist-info", ".data"))
|
|
63
|
+
and not c.name.startswith(".")
|
|
64
|
+
]
|
|
65
|
+
candidates = [
|
|
66
|
+
*level,
|
|
67
|
+
*(c for parent in level for c in sorted(parent.iterdir()) if c.is_dir()),
|
|
68
|
+
]
|
|
69
|
+
for found in (_declares_collection, _has_templates):
|
|
70
|
+
for candidate in candidates:
|
|
71
|
+
if found(candidate):
|
|
72
|
+
return candidate
|
|
73
|
+
return checkout_root
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _has_templates(directory: Path) -> bool:
|
|
77
|
+
return (directory / "templates").is_dir()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _declares_collection(directory: Path) -> bool:
|
|
81
|
+
templates = directory / "templates"
|
|
82
|
+
return (templates / "manifest.json").is_file() or (templates / "_manifests").is_dir()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def install_template_bundle(
|
|
86
|
+
*,
|
|
87
|
+
repo_root: Path,
|
|
88
|
+
source_uri: str,
|
|
89
|
+
bundle_name: str,
|
|
90
|
+
ref: str = "",
|
|
91
|
+
checkout_root: Path | None = None,
|
|
92
|
+
) -> BundleInstallResult:
|
|
93
|
+
"""Fetch *source_uri* and install the named bundle into *repo_root*.
|
|
94
|
+
|
|
95
|
+
Returns a :class:`BundleInstallResult`; ``result.success`` is ``False`` with
|
|
96
|
+
a descriptive error if the bundle cannot be located.
|
|
97
|
+
|
|
98
|
+
*checkout_root*, when given, is an already-fetched tree for *source_uri* (see
|
|
99
|
+
:func:`~katalyst_taxonomy.sources.bundle_sources.fetch_bundle_collection`), so a
|
|
100
|
+
caller installing several bundles fetches — and clones — the source once.
|
|
101
|
+
"""
|
|
102
|
+
# Issue #92: honour the documented contract — return an error result rather
|
|
103
|
+
# than letting raw exceptions from source fetching propagate.
|
|
104
|
+
if checkout_root is None:
|
|
105
|
+
try:
|
|
106
|
+
checkout_root = fetch_source_tree(source_uri, ref)
|
|
107
|
+
except (ValueError, OSError, RuntimeError, subprocess.CalledProcessError) as exc:
|
|
108
|
+
return _failed(bundle_name, f"Failed to fetch source {source_uri!r}: {exc}")
|
|
109
|
+
checkout_root = collection_root(checkout_root)
|
|
110
|
+
try:
|
|
111
|
+
layout = read_layout_spec(checkout_root)
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
return _failed(bundle_name, str(exc))
|
|
114
|
+
if layout is None:
|
|
115
|
+
return _failed(
|
|
116
|
+
bundle_name,
|
|
117
|
+
f"Source {source_uri} declares no template collection "
|
|
118
|
+
"(looked for templates/manifest.json with layout 'wing/kind/template')",
|
|
119
|
+
)
|
|
120
|
+
manifest = layout.manifest_for(bundle_name)
|
|
121
|
+
try:
|
|
122
|
+
doc = load_manifest(manifest) if manifest.is_file() else {}
|
|
123
|
+
except yaml.YAMLError as exc:
|
|
124
|
+
return _failed(bundle_name, f"{manifest} could not be parsed: {exc}")
|
|
125
|
+
if not is_composed_manifest(doc):
|
|
126
|
+
return _failed(
|
|
127
|
+
bundle_name,
|
|
128
|
+
f"Bundle '{bundle_name}' not found in source {source_uri} "
|
|
129
|
+
f"(looked for {manifest.relative_to(checkout_root)} listing spec.templates)",
|
|
130
|
+
)
|
|
131
|
+
return install_composed_bundle(manifest=manifest, doc=doc, layout=layout, repo_root=repo_root)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _failed(name: str, error: str) -> BundleInstallResult:
|
|
135
|
+
return BundleInstallResult(name=name, version="0.0.0", installed_targets=(), errors=(error,))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _source_tree(source: str, *, fetch: bool = False) -> Path | None:
|
|
139
|
+
"""The tree *source* names: the local path, the source cache, or (with *fetch*) fetched."""
|
|
140
|
+
from katalyst_taxonomy.sources.bundle_sources import local_source_root, parse_bundle_uri
|
|
141
|
+
from katalyst_taxonomy.sources.cache import lookup
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
if source.strip().startswith(("git:", "github:", "pypi:")):
|
|
145
|
+
found = lookup(source)
|
|
146
|
+
if found is not None:
|
|
147
|
+
return found.path
|
|
148
|
+
return fetch_source_tree(source) if fetch else None
|
|
149
|
+
return local_source_root(parse_bundle_uri(source))
|
|
150
|
+
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError):
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def layout_for(
|
|
155
|
+
source: str, cache: dict[str, LayoutSpec | None] | None = None, *, fetch: bool = False
|
|
156
|
+
) -> LayoutSpec | None:
|
|
157
|
+
"""The collection layout *source* holds, read once per *cache*; None if not at hand."""
|
|
158
|
+
cache = {} if cache is None else cache
|
|
159
|
+
if source not in cache:
|
|
160
|
+
tree = _source_tree(source, fetch=fetch)
|
|
161
|
+
try:
|
|
162
|
+
cache[source] = read_layout_spec(collection_root(tree)) if tree is not None else None
|
|
163
|
+
except ValueError:
|
|
164
|
+
cache[source] = None
|
|
165
|
+
return cache[source]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def bundle_installed(
|
|
169
|
+
repo_root: Path, name: str, source: str, cache: dict[str, LayoutSpec | None] | None = None
|
|
170
|
+
) -> bool:
|
|
171
|
+
"""Whether every path bundle *name* of *source* installs is on disk in *repo_root*."""
|
|
172
|
+
layout = layout_for(source, cache)
|
|
173
|
+
return layout is not None and composed_bundle_installed(
|
|
174
|
+
layout.manifest_for(name), layout, repo_root
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def installed_bundles(repo_root: Path, declared: Mapping[str, str]) -> list[str]:
|
|
179
|
+
"""The bundles of *declared* (name → source) that *repo_root* has installed.
|
|
180
|
+
|
|
181
|
+
The ``PluginContext.template_listers`` entry: the one answer to "which template
|
|
182
|
+
bundles are installed". A source that is not at hand reports nothing, and installing
|
|
183
|
+
it again is harmless.
|
|
184
|
+
"""
|
|
185
|
+
cache: dict[str, LayoutSpec | None] = {}
|
|
186
|
+
return sorted(n for n, s in declared.items() if bundle_installed(repo_root, n, s, cache))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def describe_bundle(repo_root: Path, name: str, source: str) -> dict[str, Any] | None:
|
|
190
|
+
"""What bundle *name* of *source* puts in *repo_root*: its node kinds and files.
|
|
191
|
+
|
|
192
|
+
A ``PluginContext.template_describers`` entry. None when the source is not at hand
|
|
193
|
+
or the manifest does not resolve.
|
|
194
|
+
"""
|
|
195
|
+
layout = layout_for(source)
|
|
196
|
+
if layout is None:
|
|
197
|
+
return None
|
|
198
|
+
manifest = layout.manifest_for(name)
|
|
199
|
+
targets = bundle_targets(manifest, layout, repo_root)
|
|
200
|
+
if targets is None:
|
|
201
|
+
return None
|
|
202
|
+
kinds: dict[str, int] = {}
|
|
203
|
+
spec = cast(dict[str, Any], load_manifest(manifest).get("spec") or {})
|
|
204
|
+
for value in cast(list[object], spec.get("templates") or []):
|
|
205
|
+
kind = TemplateRef.parse(value).kind
|
|
206
|
+
kinds[kind] = kinds.get(kind, 0) + 1
|
|
207
|
+
files: list[str] = []
|
|
208
|
+
for target in targets:
|
|
209
|
+
try:
|
|
210
|
+
files.append(target.relative_to(repo_root).as_posix())
|
|
211
|
+
except ValueError:
|
|
212
|
+
files.append(str(target))
|
|
213
|
+
return {"kinds": kinds, "files": sorted(set(files))}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def install_template_for_plugin(
|
|
217
|
+
repo_root: Path,
|
|
218
|
+
source: str,
|
|
219
|
+
bundle: str,
|
|
220
|
+
*,
|
|
221
|
+
ref: str = "",
|
|
222
|
+
checkout_root: Path | None = None,
|
|
223
|
+
) -> dict[str, Any]:
|
|
224
|
+
""":func:`install_template_bundle` as a ``PluginContext.template_installers`` entry."""
|
|
225
|
+
result = install_template_bundle(
|
|
226
|
+
repo_root=repo_root,
|
|
227
|
+
source_uri=source,
|
|
228
|
+
bundle_name=bundle,
|
|
229
|
+
ref=ref,
|
|
230
|
+
checkout_root=checkout_root,
|
|
231
|
+
)
|
|
232
|
+
return {
|
|
233
|
+
"name": result.name,
|
|
234
|
+
"version": result.version,
|
|
235
|
+
"success": result.success,
|
|
236
|
+
"installed_targets": list(result.installed_targets),
|
|
237
|
+
"errors": list(result.errors),
|
|
238
|
+
}
|