BoilerWorks

boilerworks / boilerworks / manifest.py
Blame History Raw 90 lines
1
"""Pydantic models for boilerworks.yaml manifest."""
2
3
from __future__ import annotations
4
5
import re
6
from pathlib import Path
7
from typing import Literal
8
9
import yaml
10
from pydantic import BaseModel, Field, field_validator, model_validator
11
12
_SLUG_RE = re.compile(r"^[a-z][a-z0-9-]*$")
13
14
15
class ServicesConfig(BaseModel):
16
email: Literal["ses", "sendgrid", "mailgun", "none"] | None = None
17
storage: Literal["s3", "gcs", "azure-blob", "none"] | None = None
18
search: Literal["opensearch", "meilisearch", "none"] | None = None
19
cache: Literal["redis", "memcached", "none"] | None = "redis"
20
21
22
class DataConfig(BaseModel):
23
database: Literal["postgres", "mysql", "sqlite"] = "postgres"
24
migrations: bool = True
25
seed_data: bool = True
26
27
28
class TestingConfig(BaseModel):
29
e2e: Literal["playwright", "cypress", "none"] | None = None
30
unit: bool = True
31
integration: bool = True
32
33
34
class BoilerworksManifest(BaseModel):
35
project: str
36
family: str
37
size: Literal["full", "micro", "edge"]
38
topology: Literal["standard", "omni", "api-only"] = "standard"
39
cloud: Literal["aws", "gcp", "azure"] | None = None
40
ops: bool = False
41
region: str | None = None
42
domain: str | None = None
43
mobile: bool = False
44
web_presence: bool = False
45
compliance: list[str] = Field(default_factory=list)
46
services: ServicesConfig = Field(default_factory=ServicesConfig)
47
data: DataConfig = Field(default_factory=DataConfig)
48
testing: TestingConfig = Field(default_factory=TestingConfig)
49
template_versions: dict[str, str] = Field(default_factory=dict)
50
51
@field_validator("project")
52
@classmethod
53
def validate_project_slug(cls, v: str) -> str:
54
if not _SLUG_RE.match(v):
55
raise ValueError(
56
f"project name '{v}' must be lowercase, start with a letter, "
57
"and contain only letters, digits, and hyphens"
58
)
59
return v
60
61
@model_validator(mode="after")
62
def validate_family_in_registry(self) -> BoilerworksManifest:
63
from boilerworks.registry import Registry
64
65
registry = Registry()
66
if registry.get_by_name(self.family) is None:
67
valid = ", ".join(sorted(registry.names()))
68
raise ValueError(f"unknown template family '{self.family}'. Valid families: {valid}")
69
return self
70
71
def to_yaml(self) -> str:
72
"""Serialise the manifest to a YAML string."""
73
data = self.model_dump(exclude_none=False)
74
return yaml.dump(data, default_flow_style=False, sort_keys=False, allow_unicode=True)
75
76
@classmethod
77
def from_yaml(cls, text: str) -> BoilerworksManifest:
78
"""Parse a manifest from a YAML string."""
79
data = yaml.safe_load(text)
80
return cls(**data)
81
82
@classmethod
83
def from_file(cls, path: str | Path) -> BoilerworksManifest:
84
"""Load a manifest from a file path."""
85
return cls.from_yaml(Path(path).read_text())
86
87
def to_file(self, path: str | Path) -> None:
88
"""Write the manifest to a file."""
89
Path(path).write_text(self.to_yaml())
90

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button