rigyard 1.0.0__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.
- rigyard/__init__.py +19 -0
- rigyard/__main__.py +3 -0
- rigyard/application/__init__.py +30 -0
- rigyard/application/builds.py +60 -0
- rigyard/application/containers.py +63 -0
- rigyard/application/definitions.py +29 -0
- rigyard/application/images.py +61 -0
- rigyard/application/parameters.py +301 -0
- rigyard/application/project.py +77 -0
- rigyard/application/requests.py +70 -0
- rigyard/application/resolution.py +56 -0
- rigyard/application/scenarios.py +492 -0
- rigyard/application/tasks.py +57 -0
- rigyard/application/tests.py +58 -0
- rigyard/builds/__init__.py +5 -0
- rigyard/builds/backend.py +13 -0
- rigyard/builds/models.py +55 -0
- rigyard/builds/planner.py +25 -0
- rigyard/cli/__init__.py +5 -0
- rigyard/cli/branding.py +8 -0
- rigyard/cli/build_output.py +11 -0
- rigyard/cli/command_output.py +49 -0
- rigyard/cli/commands/__init__.py +1 -0
- rigyard/cli/commands/builds.py +53 -0
- rigyard/cli/commands/containers.py +74 -0
- rigyard/cli/commands/images.py +53 -0
- rigyard/cli/commands/parameters.py +91 -0
- rigyard/cli/commands/scenarios.py +181 -0
- rigyard/cli/commands/tasks.py +55 -0
- rigyard/cli/commands/tests.py +68 -0
- rigyard/cli/commands/workspace.py +124 -0
- rigyard/cli/common.py +89 -0
- rigyard/cli/container_output.py +73 -0
- rigyard/cli/main.py +117 -0
- rigyard/cli/menu/__init__.py +6 -0
- rigyard/cli/menu/app.py +477 -0
- rigyard/cli/menu/catalog.py +71 -0
- rigyard/cli/menu/model.py +24 -0
- rigyard/cli/menu/prompt.py +83 -0
- rigyard/cli/menu/session.py +22 -0
- rigyard/cli/scenario_output.py +84 -0
- rigyard/cli/style.py +128 -0
- rigyard/cli/task_output.py +11 -0
- rigyard/cli/test_output.py +14 -0
- rigyard/config/__init__.py +34 -0
- rigyard/config/catalog.py +89 -0
- rigyard/config/loader.py +303 -0
- rigyard/config/models.py +356 -0
- rigyard/container_commands/__init__.py +11 -0
- rigyard/container_commands/models.py +108 -0
- rigyard/container_commands/planner.py +37 -0
- rigyard/containers/__init__.py +1 -0
- rigyard/containers/backend.py +20 -0
- rigyard/containers/models.py +221 -0
- rigyard/containers/planner.py +212 -0
- rigyard/containers/service.py +17 -0
- rigyard/errors.py +148 -0
- rigyard/execution/__init__.py +15 -0
- rigyard/execution/docker_exec.py +175 -0
- rigyard/execution/runner.py +60 -0
- rigyard/execution/tty.py +5 -0
- rigyard/images/__init__.py +6 -0
- rigyard/images/backend.py +19 -0
- rigyard/images/models.py +181 -0
- rigyard/images/planner.py +159 -0
- rigyard/images/service.py +26 -0
- rigyard/immutable.py +40 -0
- rigyard/parameters/__init__.py +26 -0
- rigyard/parameters/context.py +50 -0
- rigyard/parameters/models.py +99 -0
- rigyard/parameters/prompt.py +134 -0
- rigyard/parameters/resolver.py +313 -0
- rigyard/parameters/sources.py +55 -0
- rigyard/parameters/templates.py +313 -0
- rigyard/providers/__init__.py +1 -0
- rigyard/providers/docker/__init__.py +23 -0
- rigyard/providers/docker/build_backend.py +28 -0
- rigyard/providers/docker/container_backend.py +206 -0
- rigyard/providers/docker/containers.py +54 -0
- rigyard/providers/docker/dockerfile.py +5 -0
- rigyard/providers/docker/exec_backend.py +116 -0
- rigyard/providers/docker/image_backend.py +70 -0
- rigyard/providers/docker/images.py +71 -0
- rigyard/providers/docker/sources.py +19 -0
- rigyard/providers/docker/task_backend.py +29 -0
- rigyard/providers/docker/test_backend.py +29 -0
- rigyard/scenarios/__init__.py +22 -0
- rigyard/scenarios/assembly.py +24 -0
- rigyard/scenarios/executor.py +347 -0
- rigyard/scenarios/identity.py +38 -0
- rigyard/scenarios/lifecycle.py +171 -0
- rigyard/scenarios/models.py +384 -0
- rigyard/scenarios/planner.py +258 -0
- rigyard/scenarios/ports.py +87 -0
- rigyard/scenarios/process.py +131 -0
- rigyard/scenarios/records.py +136 -0
- rigyard/scenarios/runtime.py +95 -0
- rigyard/scenarios/service.py +136 -0
- rigyard/scenarios/tmux.py +193 -0
- rigyard/tasks/__init__.py +5 -0
- rigyard/tasks/backend.py +13 -0
- rigyard/tasks/models.py +75 -0
- rigyard/tasks/planner.py +21 -0
- rigyard/tests/__init__.py +21 -0
- rigyard/tests/backend.py +13 -0
- rigyard/tests/models.py +74 -0
- rigyard/tests/planner.py +28 -0
- rigyard/version.py +3 -0
- rigyard/workspace.py +359 -0
- rigyard-1.0.0.dist-info/METADATA +188 -0
- rigyard-1.0.0.dist-info/RECORD +114 -0
- rigyard-1.0.0.dist-info/WHEEL +4 -0
- rigyard-1.0.0.dist-info/entry_points.txt +2 -0
- rigyard-1.0.0.dist-info/licenses/LICENSE +22 -0
rigyard/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Configuration-driven tooling for containerized development workflows."""
|
|
2
|
+
|
|
3
|
+
from .config.loader import load_config, load_values
|
|
4
|
+
from .errors import RigyardError
|
|
5
|
+
from .parameters.context import ResolvedContext, ResolvedValue, ValueSource
|
|
6
|
+
from .parameters.resolver import RuntimeValueResolver, collect_prompts
|
|
7
|
+
from .version import __version__
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"RuntimeValueResolver",
|
|
11
|
+
"ResolvedContext",
|
|
12
|
+
"ResolvedValue",
|
|
13
|
+
"RigyardError",
|
|
14
|
+
"ValueSource",
|
|
15
|
+
"__version__",
|
|
16
|
+
"load_config",
|
|
17
|
+
"collect_prompts",
|
|
18
|
+
"load_values",
|
|
19
|
+
]
|
rigyard/__main__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Reusable application use cases shared by command and menu frontends."""
|
|
2
|
+
|
|
3
|
+
from .builds import BuildProjectUseCase
|
|
4
|
+
from .containers import CreateContainerUseCase
|
|
5
|
+
from .images import BuildImageUseCase
|
|
6
|
+
from .parameters import (
|
|
7
|
+
InspectParametersUseCase,
|
|
8
|
+
ResolveParametersUseCase,
|
|
9
|
+
ValidateConfigUseCase,
|
|
10
|
+
)
|
|
11
|
+
from .project import ProjectContext
|
|
12
|
+
from .requests import BuildImageRequest, ResolutionRequest
|
|
13
|
+
from .scenarios import PlanScenarioUseCase
|
|
14
|
+
from .tasks import ExecuteTaskUseCase
|
|
15
|
+
from .tests import ExecuteTestUseCase
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BuildImageRequest",
|
|
19
|
+
"BuildImageUseCase",
|
|
20
|
+
"BuildProjectUseCase",
|
|
21
|
+
"CreateContainerUseCase",
|
|
22
|
+
"ExecuteTestUseCase",
|
|
23
|
+
"ExecuteTaskUseCase",
|
|
24
|
+
"InspectParametersUseCase",
|
|
25
|
+
"PlanScenarioUseCase",
|
|
26
|
+
"ProjectContext",
|
|
27
|
+
"ResolutionRequest",
|
|
28
|
+
"ResolveParametersUseCase",
|
|
29
|
+
"ValidateConfigUseCase",
|
|
30
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Shared application use case for configured project builds."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from ..builds.backend import BuildBackend
|
|
9
|
+
from ..builds.models import BuildPlan, BuildResult, BuildSpec
|
|
10
|
+
from ..builds.planner import BuildPlanner
|
|
11
|
+
from ..parameters.prompt import Formatter
|
|
12
|
+
from ..parameters.sources import DynamicSources
|
|
13
|
+
from .project import project_context
|
|
14
|
+
from .requests import ResolutionRequest
|
|
15
|
+
from .resolution import resolve_definition
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BuildProjectUseCase:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
backend: BuildBackend,
|
|
22
|
+
*,
|
|
23
|
+
sources: DynamicSources | None = None,
|
|
24
|
+
formatter: Formatter | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.backend = backend
|
|
27
|
+
self.sources = dict(sources or {})
|
|
28
|
+
self.formatter = formatter
|
|
29
|
+
|
|
30
|
+
def plan(
|
|
31
|
+
self,
|
|
32
|
+
build_name: str,
|
|
33
|
+
request: ResolutionRequest,
|
|
34
|
+
*,
|
|
35
|
+
environment: Mapping[str, str] | None = None,
|
|
36
|
+
now: datetime | None = None,
|
|
37
|
+
) -> BuildPlan:
|
|
38
|
+
project = project_context(
|
|
39
|
+
request.config_path,
|
|
40
|
+
request.project,
|
|
41
|
+
environment=environment,
|
|
42
|
+
timestamp=now,
|
|
43
|
+
workspace_root=request.workspace_root,
|
|
44
|
+
)
|
|
45
|
+
spec = resolve_definition(
|
|
46
|
+
project,
|
|
47
|
+
request,
|
|
48
|
+
"builds",
|
|
49
|
+
build_name,
|
|
50
|
+
BuildSpec,
|
|
51
|
+
sources=self.sources,
|
|
52
|
+
formatter=self.formatter,
|
|
53
|
+
)
|
|
54
|
+
return BuildPlanner().create_plan(
|
|
55
|
+
build_name,
|
|
56
|
+
spec,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def execute(self, plan: BuildPlan) -> BuildResult:
|
|
60
|
+
return self.backend.execute(plan)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Shared application use case for CLI and menu container creation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from ..containers.backend import ContainerBackend
|
|
9
|
+
from ..containers.models import ContainerCreateResult, ContainerRunPlan, ContainerSpec
|
|
10
|
+
from ..containers.planner import ContainerRunPlanner
|
|
11
|
+
from ..containers.service import ContainerCreateService
|
|
12
|
+
from ..parameters.prompt import Formatter
|
|
13
|
+
from ..parameters.sources import DynamicSources
|
|
14
|
+
from .project import project_context
|
|
15
|
+
from .requests import ResolutionRequest
|
|
16
|
+
from .resolution import resolve_definition
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CreateContainerUseCase:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
backend: ContainerBackend,
|
|
23
|
+
*,
|
|
24
|
+
sources: DynamicSources | None = None,
|
|
25
|
+
formatter: Formatter | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.service = ContainerCreateService(backend)
|
|
28
|
+
self.sources = dict(sources or {})
|
|
29
|
+
self.formatter = formatter
|
|
30
|
+
|
|
31
|
+
def plan(
|
|
32
|
+
self,
|
|
33
|
+
container_name: str,
|
|
34
|
+
request: ResolutionRequest,
|
|
35
|
+
*,
|
|
36
|
+
environment: Mapping[str, str] | None = None,
|
|
37
|
+
now: datetime | None = None,
|
|
38
|
+
) -> ContainerRunPlan:
|
|
39
|
+
project = project_context(
|
|
40
|
+
request.config_path,
|
|
41
|
+
request.project,
|
|
42
|
+
environment=environment,
|
|
43
|
+
timestamp=now,
|
|
44
|
+
workspace_root=request.workspace_root,
|
|
45
|
+
)
|
|
46
|
+
spec = resolve_definition(
|
|
47
|
+
project,
|
|
48
|
+
request,
|
|
49
|
+
"containers",
|
|
50
|
+
container_name,
|
|
51
|
+
ContainerSpec,
|
|
52
|
+
sources=self.sources,
|
|
53
|
+
formatter=self.formatter,
|
|
54
|
+
)
|
|
55
|
+
return ContainerRunPlanner().plan(
|
|
56
|
+
container_name,
|
|
57
|
+
spec,
|
|
58
|
+
project.config_path,
|
|
59
|
+
project.environment,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def execute(self, plan: ContainerRunPlan) -> ContainerCreateResult:
|
|
63
|
+
return self.service.create(plan)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Application access to the canonical source-qualified definition catalog."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..config.catalog import Definition, DefinitionCatalog
|
|
8
|
+
from ..config.models import RigyardConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def find_definition(
|
|
12
|
+
config: RigyardConfig,
|
|
13
|
+
kind: str,
|
|
14
|
+
name: str,
|
|
15
|
+
source_path: Path | None,
|
|
16
|
+
config_path: Path,
|
|
17
|
+
) -> Definition | None:
|
|
18
|
+
return DefinitionCatalog(config, config_path).find(kind, name, source_path)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def definition_source_paths(
|
|
22
|
+
config: RigyardConfig,
|
|
23
|
+
config_path: Path,
|
|
24
|
+
source_path: Path | None = None,
|
|
25
|
+
) -> dict[str, Path]:
|
|
26
|
+
return {
|
|
27
|
+
item.prefix: item.source_path
|
|
28
|
+
for item in DefinitionCatalog(config, config_path).selected(source_path)
|
|
29
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Application use case for building a configured image."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from ..images.backend import ImageBuildBackend
|
|
9
|
+
from ..images.models import ImageBuildPlan, ImageBuildResult, ImageSpec
|
|
10
|
+
from ..images.planner import ImageBuildPlanner
|
|
11
|
+
from ..images.service import ImageBuildService
|
|
12
|
+
from ..parameters.prompt import Formatter
|
|
13
|
+
from ..parameters.sources import DynamicSources
|
|
14
|
+
from .project import project_context
|
|
15
|
+
from .requests import BuildImageRequest
|
|
16
|
+
from .resolution import resolve_definition
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BuildImageUseCase:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
backend: ImageBuildBackend,
|
|
23
|
+
*,
|
|
24
|
+
sources: DynamicSources | None = None,
|
|
25
|
+
formatter: Formatter | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.backend = backend
|
|
28
|
+
self.sources = dict(sources or {})
|
|
29
|
+
self.formatter = formatter
|
|
30
|
+
|
|
31
|
+
def plan(
|
|
32
|
+
self,
|
|
33
|
+
request: BuildImageRequest,
|
|
34
|
+
*,
|
|
35
|
+
environment: Mapping[str, str] | None = None,
|
|
36
|
+
now: datetime | None = None,
|
|
37
|
+
) -> ImageBuildPlan:
|
|
38
|
+
project = project_context(
|
|
39
|
+
request.config_path,
|
|
40
|
+
request.project,
|
|
41
|
+
environment=environment,
|
|
42
|
+
timestamp=now,
|
|
43
|
+
workspace_root=request.workspace_root,
|
|
44
|
+
)
|
|
45
|
+
spec = resolve_definition(
|
|
46
|
+
project,
|
|
47
|
+
request.resolution_request(),
|
|
48
|
+
"images",
|
|
49
|
+
request.image_name,
|
|
50
|
+
ImageSpec,
|
|
51
|
+
sources=self.sources,
|
|
52
|
+
formatter=self.formatter,
|
|
53
|
+
)
|
|
54
|
+
return ImageBuildPlanner().create_plan(
|
|
55
|
+
request.image_name,
|
|
56
|
+
spec,
|
|
57
|
+
project.config_path,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def execute(self, plan: ImageBuildPlan) -> ImageBuildResult:
|
|
61
|
+
return ImageBuildService(self.backend).build(plan)
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Application boundary for inline runtime value discovery and resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from ..builds.models import BuildSpec
|
|
12
|
+
from ..config.catalog import DefinitionCatalog
|
|
13
|
+
from ..config.loader import load_config, load_values
|
|
14
|
+
from ..config.models import RigyardConfig
|
|
15
|
+
from ..containers.models import ContainerSpec
|
|
16
|
+
from ..errors import ResolutionError
|
|
17
|
+
from ..images.models import ImageSpec
|
|
18
|
+
from ..parameters.context import ResolvedContext
|
|
19
|
+
from ..parameters.prompt import Formatter
|
|
20
|
+
from ..parameters.resolver import (
|
|
21
|
+
RuntimeValueResolver,
|
|
22
|
+
collect_prompts,
|
|
23
|
+
flatten_values,
|
|
24
|
+
materialize_as,
|
|
25
|
+
)
|
|
26
|
+
from ..parameters.sources import DynamicSources
|
|
27
|
+
from ..parameters.templates import (
|
|
28
|
+
SourceAwareStringTemplateRenderer,
|
|
29
|
+
StringTemplateRenderer,
|
|
30
|
+
TemplateContext,
|
|
31
|
+
validate_template_syntax,
|
|
32
|
+
)
|
|
33
|
+
from ..scenarios.models import (
|
|
34
|
+
ScenarioComposeSpec,
|
|
35
|
+
ScenarioInstanceSpec,
|
|
36
|
+
ScenarioProfileSpec,
|
|
37
|
+
ScenarioStartupSpec,
|
|
38
|
+
ScenarioTemplate,
|
|
39
|
+
)
|
|
40
|
+
from ..tasks.models import TaskSpec
|
|
41
|
+
from ..tests.models import TestSpec
|
|
42
|
+
from .definitions import definition_source_paths
|
|
43
|
+
from .project import project_context
|
|
44
|
+
from .requests import ResolutionRequest
|
|
45
|
+
|
|
46
|
+
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ValidateConfigUseCase:
|
|
50
|
+
def execute(
|
|
51
|
+
self,
|
|
52
|
+
config_path: str | Path,
|
|
53
|
+
*,
|
|
54
|
+
workspace_root: str | Path | None = None,
|
|
55
|
+
) -> RigyardConfig:
|
|
56
|
+
config = load_config(config_path, workspace_root=workspace_root)
|
|
57
|
+
_validate_config_templates(config, config_path)
|
|
58
|
+
return config
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class InspectParametersUseCase:
|
|
62
|
+
def execute(
|
|
63
|
+
self,
|
|
64
|
+
config_path: str | Path,
|
|
65
|
+
source_path: Path | None = None,
|
|
66
|
+
*,
|
|
67
|
+
workspace_root: str | Path | None = None,
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
config = load_config(config_path, workspace_root=workspace_root)
|
|
70
|
+
_validate_config_templates(config, config_path)
|
|
71
|
+
prompts = _collect_config_prompts(config, Path(config_path), source_path)
|
|
72
|
+
return {
|
|
73
|
+
"version": config.version,
|
|
74
|
+
"runtime_values": RuntimeValueResolver(prompts).inspect(),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ResolveParametersUseCase:
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
sources: DynamicSources | None = None,
|
|
82
|
+
formatter: Formatter | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
self.sources = dict(sources or {})
|
|
85
|
+
self.formatter = formatter
|
|
86
|
+
|
|
87
|
+
def execute(
|
|
88
|
+
self, request: ResolutionRequest, *, allow_missing: bool = False
|
|
89
|
+
) -> ResolvedContext:
|
|
90
|
+
project = project_context(
|
|
91
|
+
request.config_path,
|
|
92
|
+
request.project,
|
|
93
|
+
workspace_root=request.workspace_root,
|
|
94
|
+
)
|
|
95
|
+
config = project.config
|
|
96
|
+
template_context = TemplateContext.capture(
|
|
97
|
+
project.environment,
|
|
98
|
+
now=project.timestamp,
|
|
99
|
+
config_path=project.config_path,
|
|
100
|
+
workspace_root=project.workspace_root,
|
|
101
|
+
variables=config.variables,
|
|
102
|
+
)
|
|
103
|
+
renderer = SourceAwareStringTemplateRenderer(
|
|
104
|
+
template_context,
|
|
105
|
+
definition_source_paths(config, project.config_path, request.source_path),
|
|
106
|
+
)
|
|
107
|
+
context = resolve_prompts(
|
|
108
|
+
config,
|
|
109
|
+
request,
|
|
110
|
+
allow_missing=allow_missing,
|
|
111
|
+
renderer=renderer,
|
|
112
|
+
sources=self.sources,
|
|
113
|
+
formatter=self.formatter,
|
|
114
|
+
environment=project.environment,
|
|
115
|
+
)
|
|
116
|
+
if not allow_missing:
|
|
117
|
+
targets: dict[str, type[BaseModel]] = {
|
|
118
|
+
"images": ImageSpec,
|
|
119
|
+
"containers": ContainerSpec,
|
|
120
|
+
"builds": BuildSpec,
|
|
121
|
+
"tests": TestSpec,
|
|
122
|
+
"tasks": TaskSpec,
|
|
123
|
+
}
|
|
124
|
+
for entry in DefinitionCatalog(config, project.config_path).selected(
|
|
125
|
+
request.source_path
|
|
126
|
+
):
|
|
127
|
+
if isinstance(entry.value, ScenarioTemplate):
|
|
128
|
+
scenario = entry.value
|
|
129
|
+
if scenario.compose is not None:
|
|
130
|
+
materialize_as(
|
|
131
|
+
scenario.compose,
|
|
132
|
+
f"{entry.prefix}.compose",
|
|
133
|
+
context,
|
|
134
|
+
ScenarioComposeSpec,
|
|
135
|
+
renderer,
|
|
136
|
+
)
|
|
137
|
+
materialize_as(
|
|
138
|
+
scenario.startup,
|
|
139
|
+
f"{entry.prefix}.startup",
|
|
140
|
+
context,
|
|
141
|
+
ScenarioStartupSpec,
|
|
142
|
+
renderer,
|
|
143
|
+
)
|
|
144
|
+
for name, instance in scenario.instances.items():
|
|
145
|
+
materialize_as(
|
|
146
|
+
instance,
|
|
147
|
+
f"{entry.prefix}.instances.{name}",
|
|
148
|
+
context,
|
|
149
|
+
ScenarioInstanceSpec,
|
|
150
|
+
renderer,
|
|
151
|
+
)
|
|
152
|
+
for name, profile in scenario.profiles.items():
|
|
153
|
+
materialize_as(
|
|
154
|
+
profile,
|
|
155
|
+
f"{entry.prefix}.profiles.{name}",
|
|
156
|
+
context,
|
|
157
|
+
ScenarioProfileSpec,
|
|
158
|
+
renderer,
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
materialize_as(
|
|
162
|
+
entry.value, entry.prefix, context, targets[entry.kind], renderer
|
|
163
|
+
)
|
|
164
|
+
return context
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _validate_config_templates(config: RigyardConfig, config_path: str | Path) -> None:
|
|
168
|
+
base = TemplateContext.capture({}, config_path=config_path)
|
|
169
|
+
previous: list[str] = []
|
|
170
|
+
for name, value in config.variables.items():
|
|
171
|
+
StringTemplateRenderer(base, variable_names=previous).validate(value, f"variables.{name}")
|
|
172
|
+
previous.append(name)
|
|
173
|
+
renderer = StringTemplateRenderer(base, variable_names=config.variables)
|
|
174
|
+
for entry in DefinitionCatalog(config, Path(config_path)).entries:
|
|
175
|
+
validate_template_syntax(entry.value, entry.prefix, renderer=renderer)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def resolve_prompts(
|
|
179
|
+
template: BaseModel,
|
|
180
|
+
request: ResolutionRequest,
|
|
181
|
+
*,
|
|
182
|
+
prefix: str = "",
|
|
183
|
+
allow_missing: bool = False,
|
|
184
|
+
renderer: StringTemplateRenderer | None = None,
|
|
185
|
+
sources: DynamicSources | None = None,
|
|
186
|
+
formatter: Formatter | None = None,
|
|
187
|
+
environment: Mapping[str, str] | None = None,
|
|
188
|
+
) -> ResolvedContext:
|
|
189
|
+
prompts = (
|
|
190
|
+
_collect_config_prompts(template, request.config_path, request.source_path)
|
|
191
|
+
if isinstance(template, RigyardConfig) and not prefix
|
|
192
|
+
else collect_prompts(template, prefix)
|
|
193
|
+
)
|
|
194
|
+
values = load_values(request.values_path) if request.values_path else {}
|
|
195
|
+
return RuntimeValueResolver(
|
|
196
|
+
prompts,
|
|
197
|
+
render_default=default_display_renderer(renderer),
|
|
198
|
+
sources=sources,
|
|
199
|
+
formatter=formatter,
|
|
200
|
+
).resolve(
|
|
201
|
+
values=values,
|
|
202
|
+
environ=environment if environment is not None else _request_environment(request),
|
|
203
|
+
overrides=request.overrides,
|
|
204
|
+
interactive=request.interactive,
|
|
205
|
+
input_fn=request.input_fn,
|
|
206
|
+
allow_missing=allow_missing,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _collect_config_prompts(
|
|
211
|
+
config: RigyardConfig,
|
|
212
|
+
config_path: Path,
|
|
213
|
+
source_path: Path | None = None,
|
|
214
|
+
) -> dict[str, Any]:
|
|
215
|
+
prompts: dict[str, Any] = {}
|
|
216
|
+
for entry in DefinitionCatalog(config, config_path).selected(source_path):
|
|
217
|
+
prompts.update(collect_prompts(entry.value, entry.prefix))
|
|
218
|
+
return prompts
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _available_prompts(root: BaseModel, request: ResolutionRequest) -> dict[str, Any]:
|
|
222
|
+
if not isinstance(root, RigyardConfig):
|
|
223
|
+
return collect_prompts(root)
|
|
224
|
+
prompts: dict[str, Any] = {}
|
|
225
|
+
for entry in DefinitionCatalog(root, request.config_path).entries:
|
|
226
|
+
prompts.update(collect_prompts(entry.value, entry.prefix))
|
|
227
|
+
return prompts
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def resolve_template(
|
|
231
|
+
root: BaseModel,
|
|
232
|
+
template: BaseModel,
|
|
233
|
+
request: ResolutionRequest,
|
|
234
|
+
prefix: str,
|
|
235
|
+
target: type[ModelT],
|
|
236
|
+
renderer: StringTemplateRenderer | None = None,
|
|
237
|
+
sources: DynamicSources | None = None,
|
|
238
|
+
formatter: Formatter | None = None,
|
|
239
|
+
environment: Mapping[str, str] | None = None,
|
|
240
|
+
) -> tuple[ModelT, ResolvedContext]:
|
|
241
|
+
selected = collect_prompts(template, prefix)
|
|
242
|
+
context = resolve_selected_prompts(
|
|
243
|
+
root,
|
|
244
|
+
selected,
|
|
245
|
+
request,
|
|
246
|
+
renderer=renderer,
|
|
247
|
+
sources=sources,
|
|
248
|
+
formatter=formatter,
|
|
249
|
+
environment=environment,
|
|
250
|
+
)
|
|
251
|
+
return materialize_as(template, prefix, context, target, renderer), context
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def resolve_selected_prompts(
|
|
255
|
+
root: BaseModel,
|
|
256
|
+
selected: dict[str, Any],
|
|
257
|
+
request: ResolutionRequest,
|
|
258
|
+
*,
|
|
259
|
+
renderer: StringTemplateRenderer | None = None,
|
|
260
|
+
sources: DynamicSources | None = None,
|
|
261
|
+
formatter: Formatter | None = None,
|
|
262
|
+
environment: Mapping[str, str] | None = None,
|
|
263
|
+
) -> ResolvedContext:
|
|
264
|
+
"""Resolve an explicitly composed set of prompt paths from one config root."""
|
|
265
|
+
|
|
266
|
+
available = _available_prompts(root, request)
|
|
267
|
+
available.update(selected)
|
|
268
|
+
values = load_values(request.values_path) if request.values_path else {}
|
|
269
|
+
flat_values = flatten_values(values)
|
|
270
|
+
unknown = (set(flat_values) | set(request.overrides)) - set(available)
|
|
271
|
+
if unknown:
|
|
272
|
+
raise ResolutionError(f"unknown runtime value(s): {', '.join(sorted(unknown))}")
|
|
273
|
+
return RuntimeValueResolver(
|
|
274
|
+
selected,
|
|
275
|
+
render_default=default_display_renderer(renderer),
|
|
276
|
+
sources=sources,
|
|
277
|
+
formatter=formatter,
|
|
278
|
+
).resolve(
|
|
279
|
+
values={key: value for key, value in flat_values.items() if key in selected},
|
|
280
|
+
environ=environment if environment is not None else _request_environment(request),
|
|
281
|
+
overrides={key: value for key, value in request.overrides.items() if key in selected},
|
|
282
|
+
interactive=request.interactive,
|
|
283
|
+
input_fn=request.input_fn,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _request_environment(request: ResolutionRequest) -> Mapping[str, str] | None:
|
|
288
|
+
return request.project.environment if request.project is not None else None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def default_display_renderer(
|
|
292
|
+
renderer: StringTemplateRenderer | None,
|
|
293
|
+
) -> Callable[[str, Any], str] | None:
|
|
294
|
+
"""Adapt a template renderer for rendering prompt defaults in hints."""
|
|
295
|
+
if renderer is None:
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
def render(path: str, value: Any) -> str:
|
|
299
|
+
return str(renderer.render_value(value, path))
|
|
300
|
+
|
|
301
|
+
return render
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""A stable project snapshot shared across one frontend operation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
|
|
12
|
+
from ..config.loader import load_config
|
|
13
|
+
from ..config.models import RigyardConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ProjectContext:
|
|
18
|
+
"""Configuration and host context captured together at one point in time."""
|
|
19
|
+
|
|
20
|
+
config_path: Path
|
|
21
|
+
config: RigyardConfig
|
|
22
|
+
environment: Mapping[str, str]
|
|
23
|
+
timestamp: datetime
|
|
24
|
+
workspace_root: Path = field(default_factory=lambda: Path.cwd().resolve())
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
object.__setattr__(self, "config_path", Path(self.config_path))
|
|
28
|
+
object.__setattr__(self, "workspace_root", Path(self.workspace_root).resolve())
|
|
29
|
+
object.__setattr__(self, "environment", MappingProxyType(dict(self.environment)))
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def capture(
|
|
33
|
+
cls,
|
|
34
|
+
config_path: str | Path,
|
|
35
|
+
*,
|
|
36
|
+
config: RigyardConfig | None = None,
|
|
37
|
+
environment: Mapping[str, str] | None = None,
|
|
38
|
+
timestamp: datetime | None = None,
|
|
39
|
+
workspace_root: str | Path | None = None,
|
|
40
|
+
) -> ProjectContext:
|
|
41
|
+
path = Path(config_path)
|
|
42
|
+
root = Path.cwd().resolve() if workspace_root is None else Path(workspace_root).resolve()
|
|
43
|
+
return cls(
|
|
44
|
+
config_path=path,
|
|
45
|
+
config=load_config(path, workspace_root=root) if config is None else config,
|
|
46
|
+
environment=os.environ if environment is None else environment,
|
|
47
|
+
timestamp=datetime.now().astimezone() if timestamp is None else timestamp,
|
|
48
|
+
workspace_root=root,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def project_context(
|
|
53
|
+
config_path: str | Path,
|
|
54
|
+
current: ProjectContext | None,
|
|
55
|
+
*,
|
|
56
|
+
environment: Mapping[str, str] | None = None,
|
|
57
|
+
timestamp: datetime | None = None,
|
|
58
|
+
workspace_root: str | Path | None = None,
|
|
59
|
+
) -> ProjectContext:
|
|
60
|
+
"""Reuse a supplied snapshot while honoring explicit test/runtime overrides."""
|
|
61
|
+
|
|
62
|
+
if current is None:
|
|
63
|
+
return ProjectContext.capture(
|
|
64
|
+
config_path,
|
|
65
|
+
environment=environment,
|
|
66
|
+
timestamp=timestamp,
|
|
67
|
+
workspace_root=workspace_root,
|
|
68
|
+
)
|
|
69
|
+
if environment is None and timestamp is None and workspace_root is None:
|
|
70
|
+
return current
|
|
71
|
+
return ProjectContext(
|
|
72
|
+
config_path=current.config_path,
|
|
73
|
+
config=current.config,
|
|
74
|
+
environment=current.environment if environment is None else environment,
|
|
75
|
+
timestamp=current.timestamp if timestamp is None else timestamp,
|
|
76
|
+
workspace_root=current.workspace_root if workspace_root is None else Path(workspace_root),
|
|
77
|
+
)
|