working
This commit is contained in:
7
src/docker_compose/cfg/__init__.py
Normal file
7
src/docker_compose/cfg/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path("/nas")
|
||||
DATA_ROOT = ROOT.joinpath("apps")
|
||||
CFG_ROOT = ROOT.joinpath("cfg/templates")
|
||||
TRAEFIK_PATH = CFG_ROOT.joinpath("traefik")
|
||||
# TEMPLATE_DIR = CFG_ROOT.joinpath("templates")
|
||||
62
src/docker_compose/cfg/cfg_paths.py
Normal file
62
src/docker_compose/cfg/cfg_paths.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from itertools import chain
|
||||
from shutil import copyfile
|
||||
from typing import Self, final
|
||||
|
||||
from docker_compose.cfg.compose_paths import ComposePaths
|
||||
from docker_compose.cfg.dest_path import DestPaths
|
||||
from docker_compose.cfg.org import OrgData
|
||||
from docker_compose.cfg.src_path import SrcPaths
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CfgData:
|
||||
src_paths: SrcPaths
|
||||
org_data: OrgData
|
||||
compose_paths: ComposePaths
|
||||
dest_paths: DestPaths
|
||||
|
||||
@classmethod
|
||||
def from_src_paths(cls, src_paths: SrcPaths) -> Iterator[Self]:
|
||||
for org, args in src_paths.org_file.as_dict.items():
|
||||
org = OrgData.from_dict(src_paths.app, org, args)
|
||||
dest = DestPaths.from_org(org)
|
||||
yield cls(
|
||||
src_paths,
|
||||
org,
|
||||
ComposePaths.from_iters(
|
||||
src_paths.service_dir.files,
|
||||
src_paths.vol_dir.files,
|
||||
),
|
||||
dest,
|
||||
)
|
||||
|
||||
def pre_render(self, data: str) -> str:
|
||||
for func in chain(
|
||||
self.org_data.pre_render_funcs, self.dest_paths.pre_render_funcs
|
||||
):
|
||||
data = func(data)
|
||||
return data
|
||||
|
||||
def render(self, data: str) -> str:
|
||||
for func in chain(
|
||||
self.org_data.render_funcs,
|
||||
self.dest_paths.render_funcs,
|
||||
self.compose_paths.render_funcs,
|
||||
):
|
||||
data = func(data)
|
||||
return data
|
||||
|
||||
def render_all(self, data: str) -> str:
|
||||
for func in (self.pre_render, self.render):
|
||||
# noinspection PyArgumentList
|
||||
data = func(data)
|
||||
return data
|
||||
|
||||
def mk_compose_env(self) -> None:
|
||||
src = self.src_paths.env_file
|
||||
dest = self.dest_paths.env_file
|
||||
if src.exists() and not dest.exists():
|
||||
_ = copyfile(src, dest)
|
||||
112
src/docker_compose/cfg/compose_paths.py
Normal file
112
src/docker_compose/cfg/compose_paths.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from collections.abc import Callable, Iterable, Iterator, MutableMapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Self, cast, final, override
|
||||
|
||||
import yaml
|
||||
|
||||
from docker_compose.cfg.org import OrgData
|
||||
from docker_compose.cfg.record import Record, RecordCls, RecordName
|
||||
from docker_compose.compose.services_yaml import ServiceYamlRead
|
||||
from docker_compose.compose.volume_yaml import VolYaml
|
||||
from docker_compose.util.Ts import T_YamlRW
|
||||
from docker_compose.util.yaml_util import path_to_typed, read_yaml
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ServiceValNew:
|
||||
val: Path
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return str(RecordName(self.val.stem))
|
||||
|
||||
@property
|
||||
def replace(self) -> Record[Self, str]:
|
||||
return Record(self, self.val.stem)
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ServiceVal(RecordCls[ServiceValNew]):
|
||||
old = RecordName("service")
|
||||
|
||||
@property
|
||||
def stage_two(self):
|
||||
return self.new.replace
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ServicePath:
|
||||
fqdn = Record[RecordName, str](
|
||||
RecordName("fqdn"),
|
||||
f"{OrgData.org_app.old!s}_{ServiceVal.old!s}",
|
||||
)
|
||||
|
||||
path: Path
|
||||
replace: ServiceVal = field(init=False)
|
||||
|
||||
def __post_init__(self):
|
||||
setter = super(ServicePath, self).__setattr__
|
||||
setter("replace", ServiceVal(ServiceValNew(self.path)))
|
||||
|
||||
@property
|
||||
def as_dict(self) -> ServiceYamlRead:
|
||||
with self.path.open("rt") as f:
|
||||
data_str = f.read()
|
||||
for func in self.pre_render_funcs:
|
||||
data_str = func(data_str)
|
||||
data_dict: T_YamlRW = yaml.safe_load(data_str) # pyright: ignore[reportAny]
|
||||
if not isinstance(data_dict, MutableMapping):
|
||||
raise TypeError
|
||||
path_to_typed(ServiceYamlRead, data_dict, self.path)
|
||||
return data_dict # pyright: ignore[reportReturnType]
|
||||
|
||||
@property
|
||||
def pre_render_funcs(self) -> Iterator[Callable[[str], str]]:
|
||||
yield self.fqdn
|
||||
yield self.replace
|
||||
|
||||
@property
|
||||
def render_funcs(self) -> Iterator[Callable[[str], str]]:
|
||||
yield self.replace.stage_two
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VolumePath:
|
||||
path: Path
|
||||
|
||||
@property
|
||||
def as_k_v(self) -> tuple[str, VolYaml]:
|
||||
return self.path.stem, self.as_dict
|
||||
|
||||
@property
|
||||
def as_dict(self) -> VolYaml:
|
||||
return cast(VolYaml, cast(object, read_yaml(self.path)))
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ComposePaths:
|
||||
services: frozenset[ServicePath]
|
||||
volumes: frozenset[VolumePath]
|
||||
|
||||
@classmethod
|
||||
def from_iters(cls, services: Iterable[ServicePath], volumes: Iterable[VolumePath]):
|
||||
return cls(
|
||||
frozenset(services),
|
||||
frozenset(volumes),
|
||||
)
|
||||
|
||||
@property
|
||||
def volumes_k_v(self) -> Iterator[tuple[str, VolYaml]]:
|
||||
for path in self.volumes:
|
||||
yield path.as_k_v
|
||||
|
||||
@property
|
||||
def render_funcs(self) -> Iterator[Callable[[str], str]]:
|
||||
for path in self.services:
|
||||
yield from path.render_funcs
|
||||
81
src/docker_compose/cfg/dest_path.py
Normal file
81
src/docker_compose/cfg/dest_path.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from os import sep
|
||||
from pathlib import Path
|
||||
from typing import Self, final
|
||||
|
||||
from docker_compose.cfg import DATA_ROOT
|
||||
from docker_compose.cfg.org import AppVal, OrgData, OrgVal
|
||||
from docker_compose.cfg.record import Record, RecordName
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ComposeFileRendered:
|
||||
path: Path
|
||||
|
||||
def write(self, data: str) -> None:
|
||||
print(self.path)
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with self.path.open("wt") as f:
|
||||
_ = f.write(data)
|
||||
|
||||
|
||||
# @final
|
||||
# @dataclass(frozen=True, slots=True)
|
||||
# class DataDirReplace(RecordCls[Path]):
|
||||
# old = RecordName("data")
|
||||
#
|
||||
#
|
||||
# @final
|
||||
# @dataclass(frozen=True, slots=True)
|
||||
# class DataDir:
|
||||
# val: Path
|
||||
# replace: DataDirReplace = field(init=False)
|
||||
#
|
||||
# def __post_init__(self) -> None:
|
||||
# setter = super().__setattr__
|
||||
# setter("replace", DataDirReplace(self.val))
|
||||
#
|
||||
# @classmethod
|
||||
# def from_org(cls, org: OrgData) -> Self:
|
||||
# cls(DATA_ROOT.joinpath(org.org.val, org.app.val))
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DestPaths:
|
||||
data_root = Record[RecordName, Path](RecordName("data_root"), DATA_ROOT)
|
||||
data_path = Record[RecordName, str](
|
||||
RecordName("data"),
|
||||
sep.join((str(data_root.old), str(OrgVal.old), str(AppVal.old))),
|
||||
)
|
||||
data_dir: Path
|
||||
env_file: Path = field(init=False)
|
||||
compose_file: ComposeFileRendered = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
setter = super(DestPaths, self).__setattr__
|
||||
path_join = self.data_dir.joinpath
|
||||
setter("env_file", path_join(".env"))
|
||||
setter("compose_file", ComposeFileRendered(path_join("docker-compose.yml")))
|
||||
|
||||
@classmethod
|
||||
def from_org(cls, org: OrgData) -> Self:
|
||||
return cls.from_path(DATA_ROOT.joinpath(org.org.val, org.app.val))
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: Path) -> Self:
|
||||
return cls(path)
|
||||
|
||||
# def mk_compose_dir(self) -> None:
|
||||
# folder = self.data_dir
|
||||
# if folder.exists():
|
||||
# return
|
||||
# folder.mkdir(parents=True)
|
||||
|
||||
@property
|
||||
def pre_render_funcs(self) -> Iterator[Callable[[str], str]]:
|
||||
yield self.data_path
|
||||
|
||||
@property
|
||||
def render_funcs(self) -> Iterator[Callable[[str], str]]:
|
||||
yield self.data_root
|
||||
104
src/docker_compose/cfg/org.py
Normal file
104
src/docker_compose/cfg/org.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Callable, ClassVar, Self, final, override
|
||||
|
||||
from docker_compose.cfg.org_yaml import OrgDataYaml
|
||||
from docker_compose.cfg.record import Record, RecordCls, RecordName
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OrgVal(RecordCls[str]):
|
||||
old: ClassVar[RecordName] = RecordName("org")
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Org:
|
||||
val: str
|
||||
replace: OrgVal = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
setter = super(Org, self).__setattr__
|
||||
setter("replace", OrgVal(self.val))
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AppVal(RecordCls[str]):
|
||||
old: ClassVar[RecordName] = RecordName("name")
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class App:
|
||||
val: str
|
||||
replace: AppVal = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
setter = super(App, self).__setattr__
|
||||
setter("replace", AppVal(self.val))
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class UrlValNew:
|
||||
val: str | None
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
if not self.val:
|
||||
return ""
|
||||
return ".".join((self.val, "ccamper7", "net"))
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class UrlVal(RecordCls[UrlValNew]):
|
||||
old = RecordName("url")
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Url:
|
||||
val: str | None
|
||||
replace: UrlVal = field(init=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
setter = super(Url, self).__setattr__
|
||||
setter("replace", UrlVal(UrlValNew(self.val)))
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class OrgData:
|
||||
org_app = Record[RecordName, str](
|
||||
RecordName(f"{OrgVal.old.val}_{AppVal.old.val}"),
|
||||
f"{OrgVal.old!s}_{AppVal.old!s}",
|
||||
)
|
||||
app: App
|
||||
org: Org
|
||||
url: Url
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, app: str, org: str, data: OrgDataYaml) -> Self:
|
||||
return cls(
|
||||
App(app),
|
||||
Org(org),
|
||||
Url(data.get("url")),
|
||||
)
|
||||
|
||||
@property
|
||||
def render_funcs(self) -> Iterator[Callable[[str], str]]:
|
||||
yield self.app.replace
|
||||
yield self.org.replace
|
||||
yield self.url.replace
|
||||
|
||||
@property
|
||||
def pre_render_funcs(self) -> Iterator[Callable[[str], str]]:
|
||||
yield self.org_app
|
||||
|
||||
# def render_yaml(self, yaml: str) -> str:
|
||||
# for func in self.render_funcs:
|
||||
# yaml = func(yaml)
|
||||
# return yaml
|
||||
9
src/docker_compose/cfg/org_yaml.py
Normal file
9
src/docker_compose/cfg/org_yaml.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Literal, NotRequired, TypedDict
|
||||
|
||||
|
||||
class OrgDataYaml(TypedDict):
|
||||
# org: str
|
||||
url: NotRequired[str]
|
||||
|
||||
|
||||
type OrgYaml = dict[Literal["ccamper7", "c4", "stryten"], OrgDataYaml]
|
||||
38
src/docker_compose/cfg/record.py
Normal file
38
src/docker_compose/cfg/record.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Protocol, final, override
|
||||
|
||||
|
||||
class String(Protocol):
|
||||
@override
|
||||
def __str__(self) -> str: ...
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordName:
|
||||
val: str
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return f"${{_{str(self.val).upper()}}}"
|
||||
|
||||
# def replace(self, string: str) -> str:
|
||||
# return string.replace(str(self), str(self))
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecordCls[T_New: String]:
|
||||
old: ClassVar[String]
|
||||
new: T_New
|
||||
|
||||
def __call__(self, string: str) -> str:
|
||||
return string.replace(str(self.old), str(self.new))
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Record[T_Old: String, T_New: String]:
|
||||
old: T_Old
|
||||
new: T_New
|
||||
|
||||
def __call__(self, string: str) -> str:
|
||||
return string.replace(str(self.old), str(self.new))
|
||||
101
src/docker_compose/cfg/src_path.py
Normal file
101
src/docker_compose/cfg/src_path.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Self, cast, final
|
||||
|
||||
from docker_compose.cfg.compose_paths import ServicePath, VolumePath
|
||||
from docker_compose.cfg.org_yaml import OrgYaml
|
||||
from docker_compose.util.Ts import T_YamlDict, T_YamlRW
|
||||
from docker_compose.util.yaml_util import read_yaml, write_yaml
|
||||
|
||||
YAML_EXTS = frozenset((".yml", ".yaml"))
|
||||
|
||||
|
||||
class ComposeFileTemplate(Path):
|
||||
def write_dict(self, data: T_YamlDict) -> None:
|
||||
write_yaml(data, self)
|
||||
|
||||
def write(self, data: str) -> None:
|
||||
with self.open("wt") as f:
|
||||
_ = f.write(data)
|
||||
|
||||
|
||||
class OrgFile(Path):
|
||||
@property
|
||||
def as_dict(self) -> OrgYaml:
|
||||
return cast(OrgYaml, cast(object, read_yaml(self)))
|
||||
|
||||
|
||||
class YamlDir(Path):
|
||||
@property
|
||||
def yaml_files(self) -> Iterator[Path]:
|
||||
if not self:
|
||||
raise FileNotFoundError(self)
|
||||
for service in self.iterdir():
|
||||
if service.suffix not in YAML_EXTS:
|
||||
continue
|
||||
yield service
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.exists()
|
||||
|
||||
|
||||
class CfgDir(YamlDir):
|
||||
@property
|
||||
def cfg_file(self) -> OrgFile:
|
||||
for file in self.yaml_files:
|
||||
if file.stem != "cfg":
|
||||
continue
|
||||
return OrgFile(file)
|
||||
raise FileNotFoundError(self.joinpath("cfg.y(a)ml"))
|
||||
|
||||
|
||||
class ServiceDir(YamlDir):
|
||||
@property
|
||||
def files(self) -> Iterator[ServicePath]:
|
||||
for file in self.yaml_files:
|
||||
yield ServicePath(file)
|
||||
|
||||
|
||||
class VolumesDir(YamlDir):
|
||||
@property
|
||||
def files(self) -> Iterator[VolumePath]:
|
||||
try:
|
||||
for file in self.yaml_files:
|
||||
yield VolumePath(file)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
|
||||
class VolumeData(Path):
|
||||
def write(self, data: T_YamlRW) -> None:
|
||||
write_yaml(data, self)
|
||||
|
||||
|
||||
@final
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SrcPaths:
|
||||
cfg_dir: CfgDir
|
||||
org_file: OrgFile
|
||||
env_file: Path
|
||||
service_dir: ServiceDir
|
||||
vol_dir: VolumesDir
|
||||
compose_file: ComposeFileTemplate
|
||||
volume_data: VolumeData
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, src: Path) -> Self:
|
||||
cfg_dir = CfgDir(src)
|
||||
return cls(
|
||||
cfg_dir,
|
||||
cfg_dir.cfg_file,
|
||||
src.joinpath(".env"),
|
||||
ServiceDir(src.joinpath("services")),
|
||||
VolumesDir(src.joinpath("volumes")),
|
||||
ComposeFileTemplate(src.joinpath("docker-compose.yml")),
|
||||
VolumeData(src.joinpath("volume_paths.yml")),
|
||||
)
|
||||
|
||||
@property
|
||||
def app(self) -> str:
|
||||
return self.cfg_dir.stem
|
||||
Reference in New Issue
Block a user