From 5899992240d3ae8bf0203e0d3f36d477adc3ab78d2885405696fb0b7fc8e485c Mon Sep 17 00:00:00 2001 From: Christian Camper Date: Mon, 12 Jan 2026 22:24:07 -0600 Subject: [PATCH] sync --- .idea/.gitignore | 10 -- .idea/codeStyles/codeStyleConfig.xml | 5 - .idea/compose_gen.iml | 2 +- .idea/dictionaries/project.xml | 1 - .idea/inspectionProfiles/Project_Default.xml | 7 + .../inspectionProfiles/profiles_settings.xml | 1 - .idea/misc.xml | 8 +- .idea/workspace.xml | 95 ++++++++++ src/docker_compose/__init__.py | 43 +---- src/docker_compose/__main__.py | 28 +++ src/docker_compose/cfg/__init__.py | 7 - src/docker_compose/cfg/cfg_paths.py | 66 ------- src/docker_compose/cfg/compose_paths.py | 100 ----------- src/docker_compose/cfg/dest_path.py | 82 --------- src/docker_compose/cfg/env.py | 56 ------ src/docker_compose/cfg/org.py | 59 ------- src/docker_compose/cfg/replace.py | 70 -------- src/docker_compose/cfg/src_path.py | 101 ----------- src/docker_compose/compose/compose.py | 74 -------- src/docker_compose/compose/compose_yaml.py | 12 -- src/docker_compose/compose/rendered.py | 62 ------- src/docker_compose/compose_data/__init__.py | 4 + .../compose_data/compose_yaml.py | 12 ++ src/docker_compose/compose_data/data.py | 64 +++++++ src/docker_compose/compose_data/dest_paths.py | 17 ++ src/docker_compose/compose_data/main.py | 28 +++ .../{compose => compose_data}/net.py | 29 +-- .../{compose => compose_data}/net_yaml.py | 0 .../services.py => compose_data/service.py} | 66 +++---- .../services_yaml.py | 0 src/docker_compose/compose_data/src_paths.py | 90 ++++++++++ .../{compose => compose_data}/volume_yaml.py | 0 .../{compose => env}/__init__.py | 0 src/docker_compose/env/data.py | 44 +++++ src/docker_compose/env/main.py | 98 +++++++++++ .../docker_compose/org/__init__.py | 0 src/docker_compose/org/data.py | 45 +++++ src/docker_compose/{cfg => org}/org_yaml.py | 0 .../volumes.py => render/__init__.py} | 0 src/docker_compose/render/main.py | 165 ++++++++++++++++++ src/docker_compose/util/Ts.py | 2 +- src/docker_compose/util/replace.py | 92 ++++++++++ src/docker_compose/util/yaml_util.py | 36 ++-- 43 files changed, 871 insertions(+), 810 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/workspace.xml create mode 100644 src/docker_compose/__main__.py delete mode 100644 src/docker_compose/cfg/__init__.py delete mode 100644 src/docker_compose/cfg/cfg_paths.py delete mode 100644 src/docker_compose/cfg/compose_paths.py delete mode 100644 src/docker_compose/cfg/dest_path.py delete mode 100644 src/docker_compose/cfg/env.py delete mode 100644 src/docker_compose/cfg/org.py delete mode 100644 src/docker_compose/cfg/replace.py delete mode 100644 src/docker_compose/cfg/src_path.py delete mode 100644 src/docker_compose/compose/compose.py delete mode 100644 src/docker_compose/compose/compose_yaml.py delete mode 100644 src/docker_compose/compose/rendered.py create mode 100644 src/docker_compose/compose_data/__init__.py create mode 100644 src/docker_compose/compose_data/compose_yaml.py create mode 100644 src/docker_compose/compose_data/data.py create mode 100644 src/docker_compose/compose_data/dest_paths.py create mode 100644 src/docker_compose/compose_data/main.py rename src/docker_compose/{compose => compose_data}/net.py (64%) rename src/docker_compose/{compose => compose_data}/net_yaml.py (100%) rename src/docker_compose/{compose/services.py => compose_data/service.py} (61%) rename src/docker_compose/{compose => compose_data}/services_yaml.py (100%) create mode 100644 src/docker_compose/compose_data/src_paths.py rename src/docker_compose/{compose => compose_data}/volume_yaml.py (100%) rename src/docker_compose/{compose => env}/__init__.py (100%) create mode 100644 src/docker_compose/env/data.py create mode 100644 src/docker_compose/env/main.py rename README.md => src/docker_compose/org/__init__.py (100%) mode change 100755 => 100644 create mode 100644 src/docker_compose/org/data.py rename src/docker_compose/{cfg => org}/org_yaml.py (100%) rename src/docker_compose/{compose/volumes.py => render/__init__.py} (100%) create mode 100644 src/docker_compose/render/main.py create mode 100644 src/docker_compose/util/replace.py diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index de60c0b..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Ignored default folder with query files -/queries/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 244a3c0..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/compose_gen.iml b/.idea/compose_gen.iml index 23f40df..ac26cc3 100644 --- a/.idea/compose_gen.iml +++ b/.idea/compose_gen.iml @@ -5,7 +5,7 @@ - + \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index 239143a..8ea45c5 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -1,7 +1,6 @@ - Pswd ccamper certresolver exts diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..bcc7958 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml index 25ce8b7..449e696 100644 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -1,6 +1,5 @@ - diff --git a/.idea/misc.xml b/.idea/misc.xml index ddf20c1..0d63fbb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,11 +3,5 @@ - - - - - + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..3b2ef14 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1768276046955 + + + + \ No newline at end of file diff --git a/src/docker_compose/__init__.py b/src/docker_compose/__init__.py index 64a6fb5..7fb6820 100644 --- a/src/docker_compose/__init__.py +++ b/src/docker_compose/__init__.py @@ -1,39 +1,6 @@ -from collections.abc import Iterator -from typing import cast +from pathlib import Path -from docker_compose.cfg import CFG_ROOT, TRAEFIK_PATH -from docker_compose.compose.net_yaml import NetArgsYaml -from docker_compose.compose.rendered import Rendered -from docker_compose.util.Ts import TypeYamlCompatibleDict -from docker_compose.util.yaml_util import to_yaml - - -def load_all() -> Iterator[Rendered]: - for path in CFG_ROOT.iterdir(): - if path.stem.startswith("."): - continue - if path == TRAEFIK_PATH: - continue - yield from Rendered.from_path(path) - - -def render_all() -> Iterator[str]: - for rendered in load_all(): - rendered() - yield from rendered.proxy_nets - - -if __name__ == "__main__": - # renders = render_all() - nets = frozenset(render_all()) - traefik = next(Rendered.from_path(TRAEFIK_PATH)) - data = traefik.as_dict - data["networks"] = {net: NetArgsYaml(name=f"{net}_proxy") for net in nets} - cfg = traefik.cfg - data["services"]["traefik"]["networks"] = nets - data = cast(TypeYamlCompatibleDict, cast(object, data)) - template = cfg.pre_render(to_yaml(data)) - cfg.src_paths.compose_file.write(template) - cfg.dest_paths.compose_file.write(cfg.render(template)) - traefik.write_bind_vols() - traefik.mk_bind_vols() +ROOT = Path("/nas") +TEMPLATE_ROOT = ROOT.joinpath("templates") +APP_ROOT = ROOT.joinpath("apps") +TRAEFIK_PATH = TEMPLATE_ROOT.joinpath("traefik") diff --git a/src/docker_compose/__main__.py b/src/docker_compose/__main__.py new file mode 100644 index 0000000..28cd370 --- /dev/null +++ b/src/docker_compose/__main__.py @@ -0,0 +1,28 @@ +from collections.abc import Iterator +from typing import cast + +from docker_compose import TRAEFIK_PATH +from docker_compose.compose_data.net_yaml import NetArgsYaml +from docker_compose.render.main import RenderByApp, RenderByOrg +from docker_compose.util.Ts import TypeYamlCompatibleDict +from docker_compose.util.yaml_util import to_yaml + + +def render_all() -> Iterator[str]: + apps = RenderByApp.load_all() + apps() + return apps.proxy_nets + + +if __name__ == "__main__": + renderers = RenderByOrg.from_path(TRAEFIK_PATH) + traefik =renderers["util"] + data = traefik.template.compose_data.as_dict + nets = frozenset(render_all()) + data["networks"] = {net: NetArgsYaml(name=f"{net}_proxy") for net in nets} + data["services"]["traefik"]["networks"] = nets + data = cast(TypeYamlCompatibleDict, cast(object, data)) + + txt = traefik.write(to_yaml(data), render=True) + renderers.write_bind_vol_data() + traefik.bind_vols() diff --git a/src/docker_compose/cfg/__init__.py b/src/docker_compose/cfg/__init__.py deleted file mode 100644 index 936ede4..0000000 --- a/src/docker_compose/cfg/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from pathlib import Path - -ROOT = Path("/nas") -DATA_ROOT = ROOT.joinpath("apps") -CFG_ROOT = ROOT.joinpath("docker_templates") -TRAEFIK_PATH = CFG_ROOT.joinpath("traefik") -# TEMPLATE_DIR = CFG_ROOT.joinpath("templates") diff --git a/src/docker_compose/cfg/cfg_paths.py b/src/docker_compose/cfg/cfg_paths.py deleted file mode 100644 index ea6b72b..0000000 --- a/src/docker_compose/cfg/cfg_paths.py +++ /dev/null @@ -1,66 +0,0 @@ -from collections.abc import Iterator -from dataclasses import dataclass -from itertools import chain -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.env import Env -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( - frozenset(src_paths.service_dir.files), - frozenset(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, force: bool = False) -> None: - src = self.src_paths.env_file - dest = self.dest_paths.env_file - if not src.exists(): - return - if dest.exists() and not force: - return - Env.copy(src, dest) diff --git a/src/docker_compose/cfg/compose_paths.py b/src/docker_compose/cfg/compose_paths.py deleted file mode 100644 index 22ec3a8..0000000 --- a/src/docker_compose/cfg/compose_paths.py +++ /dev/null @@ -1,100 +0,0 @@ -from collections.abc import Callable, Iterator, MutableMapping -from dataclasses import dataclass, field -from pathlib import Path -from typing import cast, final - -import yaml - -from docker_compose.cfg.org import App, Org -from docker_compose.cfg.replace import ReplaceDynamic, ReplaceStatic, ReplaceUnique -from docker_compose.compose.services_yaml import ServiceYamlRead -from docker_compose.compose.volume_yaml import VolYaml -from docker_compose.util.Ts import TypeYamlDict -from docker_compose.util.yaml_util import path_to_typed, read_yaml - -# -# @final -# @dataclass(frozen=True, slots=True) -# class ServiceVal(ReplaceStatic): -# src = ReplaceDynamic("service") - - -@final -@dataclass(frozen=True, slots=True) -class ServicePath: - path: Path - # fqdn: ReplaceUnique = field(init=False) - replace_pre: ReplaceStatic = field(init=False) - replace_post: ReplaceStatic = field(init=False) - - def __post_init__(self): - setter = super(ServicePath, self).__setattr__ - pre, post = ServiceVal.two_stage(self.path.stem) - setter("replace_pre", pre) - setter("replace_post", post) - - # setter( - # "fqdn", - # ReplaceUnique.build_placeholder( - # "fqdn", - # Org, - # App, - # ServiceVal, - # ), - # ) - - @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: TypeYamlDict = 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 cast(ServiceYamlRead, cast(object, data_dict)) - - @property - def pre_render_funcs(self) -> Iterator[Callable[[str], str]]: - yield self.fqdn - yield self.replace_pre - - # for service in self.services: - # yield service.replace_pre - - -@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] - - @property - def render_funcs(self) -> Iterator[Callable[[str], str]]: - for service in self.services: - yield service.replace_post - - @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 diff --git a/src/docker_compose/cfg/dest_path.py b/src/docker_compose/cfg/dest_path.py deleted file mode 100644 index 561c9b9..0000000 --- a/src/docker_compose/cfg/dest_path.py +++ /dev/null @@ -1,82 +0,0 @@ -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 App, Org, OrgData -from docker_compose.cfg.replace import ReplaceUnique - - -@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("data_root", str(DATA_ROOT)) - data_root = ReplaceUnique.auto_format("data", str(DATA_ROOT)) - data_path = ReplaceUnique( - data_root.src, - sep.join((data_root.src, Org.src.fmt, App.src.fmt)), - ) - 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.dest, org.app.dest)) - - @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 diff --git a/src/docker_compose/cfg/env.py b/src/docker_compose/cfg/env.py deleted file mode 100644 index c8e178a..0000000 --- a/src/docker_compose/cfg/env.py +++ /dev/null @@ -1,56 +0,0 @@ -import re -import secrets -from collections.abc import Iterator -from dataclasses import dataclass -from functools import partial -from pathlib import Path -from typing import Self, final - -from docker_compose.cfg.replace import ReplaceDynamic, ReplaceStatic - - -@final -@dataclass(frozen=True, slots=True) -class Pswd(ReplaceStatic): - src = ReplaceDynamic("pswd") - - -@final -@dataclass -class Env: - pswd = Pswd(partial(secrets.token_urlsafe, 12)) - data: dict[str, str] - - @classmethod - def get_lines(cls, data: str) -> Iterator[tuple[str, str]]: - line_valid = re.compile(r"(^\w+)=(.+)\s*") - for line in data.splitlines(): - res = line_valid.match(line) - if not res: - continue - yield res.group(1), res.group(2) - - @classmethod - def from_path(cls, path: Path) -> Self: - with path.open(mode="rt") as f: - data = f.read() - return cls({k: v for k, v in cls.get_lines(data)}) - - @property - def with_pass(self) -> Iterator[tuple[str, str]]: - p = self.pswd - for k, v in self.data.items(): - if self.pswd.src.fmt not in v: - yield k, v - continue - yield k, p(v) - - @property - def as_txt(self) -> str: - return "\n".join(sorted(map("=".join, self.with_pass))) - - @classmethod - def copy(cls, src: Path, dest: Path) -> None: - txt = cls.from_path(src).as_txt - with dest.open(mode="wt") as f: - _ = f.write(txt) diff --git a/src/docker_compose/cfg/org.py b/src/docker_compose/cfg/org.py deleted file mode 100644 index 2dd4b2f..0000000 --- a/src/docker_compose/cfg/org.py +++ /dev/null @@ -1,59 +0,0 @@ -from collections.abc import Iterator -from dataclasses import dataclass -from typing import Callable, Self, final, override - -from docker_compose.cfg.org_yaml import OrgDataYaml -from docker_compose.cfg.replace import ReplaceDynamic, ReplaceStatic, ReplaceUnique - - -@final -@dataclass(frozen=True, slots=True) -class Org(ReplaceStatic): - src = ReplaceDynamic("org") - - -@final -@dataclass(frozen=True, slots=True) -class App(ReplaceStatic): - src = ReplaceDynamic("name") - - -@final -@dataclass(frozen=True, slots=True) -class Url(ReplaceStatic): - src = ReplaceDynamic("url") - - @property - @override - def dest(self) -> str: - val = super(Url, self).dest - if not val: - return val - return ".".join((val, "ccamper7", "net")) - - -@final -@dataclass(frozen=True, slots=True) -class OrgData: - org_app = ReplaceUnique.build_placeholder('dn', Org, App) - 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 - yield self.org - yield self.url - - @property - def pre_render_funcs(self) -> Iterator[Callable[[str], str]]: - yield self.org_app diff --git a/src/docker_compose/cfg/replace.py b/src/docker_compose/cfg/replace.py deleted file mode 100644 index 9b43533..0000000 --- a/src/docker_compose/cfg/replace.py +++ /dev/null @@ -1,70 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass, field -from typing import ClassVar, Self, final - - -def format_src(src: str) -> str: - return f"${{_{src.upper()}}}" - - -@final -@dataclass(frozen=True, slots=True) -class ReplaceUnique: - src: str - dest: str # | Callable[[], str] - - def __call__(self, string: str) -> str: - return string.replace(self.src, self.dest) - - @classmethod - def auto_format(cls, src: str, dest: str): - return cls(format_src(src), dest) - - @classmethod - def build_placeholder(cls, src: str, *dest: "type[ReplaceStatic]") -> Self: - return cls.auto_format(src, '_'.join(arg.src.fmt for arg in dest),) - - -@final -@dataclass(frozen=True, slots=True) -class ReplaceDynamic: - val: str - fmt: str - - @classmethod - def factory(cls, val:str): - return cls(val, format_src(val)) - - def __call__(self, string: str) -> str: - return string.replace(self.fmt, self.val) - - # def __str__(self) -> str: - # return self.val if isinstance(self.val, str) else self.val.fmt - # def build_placeholder(self, *args: "ReplaceDynamic"): - # data = ((rep.val.upper(), rep.fmt) for rep in chain((self,), args)) - # src: tuple[str, ...] - # dest: tuple[str, ...] - # src, dest = zip(*data) - # return ReplaceUnique("_".join(src), "_".join(dest)) - - -@dataclass(frozen=True, slots=True) -class ReplaceStatic: - src: ClassVar[ReplaceDynamic] - _dest: None | str | Callable[[], str] - - def __call__(self, string: str) -> str: - return string.replace(self.src.fmt, self.dest) - - @property - def dest(self) -> str: - if not self._dest: - return "" - if isinstance(self._dest, str): - return self._dest - return self._dest() - - @classmethod - def two_stage(cls, dest: str) -> tuple[Self, ReplaceDynamic]: - dest_var = ReplaceDynamic(dest) - return cls(dest_var.fmt), dest_var diff --git a/src/docker_compose/cfg/src_path.py b/src/docker_compose/cfg/src_path.py deleted file mode 100644 index 12ebafe..0000000 --- a/src/docker_compose/cfg/src_path.py +++ /dev/null @@ -1,101 +0,0 @@ -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 TypeYamlCompatibleDict, TypeYamlCompatibleRes -from docker_compose.util.yaml_util import read_yaml, write_yaml - -YAML_EXTS = frozenset((".yml", ".yaml")) - - -class ComposeFileTemplate(Path): - def write_dict(self, data: TypeYamlCompatibleDict) -> 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: TypeYamlCompatibleRes) -> 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 diff --git a/src/docker_compose/compose/compose.py b/src/docker_compose/compose/compose.py deleted file mode 100644 index 9327e3c..0000000 --- a/src/docker_compose/compose/compose.py +++ /dev/null @@ -1,74 +0,0 @@ -from collections.abc import Iterator -from dataclasses import dataclass -from pathlib import Path -from typing import Self, cast - -from docker_compose.cfg.cfg_paths import CfgData -from docker_compose.cfg.org import OrgData -from docker_compose.cfg.src_path import SrcPaths -from docker_compose.compose.compose_yaml import ComposeYaml -from docker_compose.compose.net import Net -from docker_compose.compose.services import Service -from docker_compose.compose.volume_yaml import VolYaml -from docker_compose.util.Ts import TypeYamlCompatibleDict -from docker_compose.util.yaml_util import to_yaml - - -@dataclass(slots=True) -class Compose: - cfg: CfgData - services: frozenset[Service] - networks: Net - volumes: dict[str, VolYaml] - # replace_args: ReplaceArgs - - @classmethod - def from_path(cls, path: Path) -> Iterator[Self]: - return cls.from_src_path(SrcPaths.from_path(path)) - - @classmethod - def from_src_path(cls, src_paths: SrcPaths) -> Iterator[Self]: - for cfg in CfgData.from_src_paths(src_paths): - yield cls.from_cfg(cfg) - - @classmethod - def from_cfg(cls, cfg_data: CfgData) -> Self: - # services = { - # path.stem: Service.from_dict(path, data) - # for path, data in cfg_data.compose_paths.services_k_v - # } - services = frozenset( - Service.from_path(path) for path in cfg_data.compose_paths.services - ) - return cls( - cfg_data, - services, - Net.from_service_list(services), - dict(cfg_data.compose_paths.volumes_k_v), - ) - - # @property - # def app(self) -> str: - # return self.cfg.src_paths.app - - @property - def as_dict(self) -> ComposeYaml: - return ComposeYaml( - name=OrgData.org_app.dest, - services={ - service.service_name: service.as_dict for service in self.services - }, - networks=self.networks.as_dict, - volumes=self.volumes, - ) - - @property - def as_template(self) -> str: - data = cast(TypeYamlCompatibleDict, cast(object, self.as_dict)) - return self.cfg.pre_render(to_yaml(data)) - - def write_template(self): - self.cfg.src_paths.compose_file.write(self.as_template) - - def __call__(self): - self.write_template() diff --git a/src/docker_compose/compose/compose_yaml.py b/src/docker_compose/compose/compose_yaml.py deleted file mode 100644 index 6f95b4b..0000000 --- a/src/docker_compose/compose/compose_yaml.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import TypedDict - -from docker_compose.compose.net_yaml import NetYaml -from docker_compose.compose.services_yaml import ServiceYamlWrite -from docker_compose.compose.volume_yaml import VolYaml - - -class ComposeYaml(TypedDict): - name: str - services: dict[str, ServiceYamlWrite] - networks: NetYaml | None - volumes: dict[str, VolYaml] | None diff --git a/src/docker_compose/compose/rendered.py b/src/docker_compose/compose/rendered.py deleted file mode 100644 index 14b9187..0000000 --- a/src/docker_compose/compose/rendered.py +++ /dev/null @@ -1,62 +0,0 @@ -from collections.abc import Iterator -from dataclasses import dataclass -from pathlib import Path -from typing import final, override - -from docker_compose.cfg import ROOT -from docker_compose.compose.compose import Compose - - -@final -@dataclass(slots=True) -class Rendered(Compose): - # @property - # def org(self) -> str: - # return self.cfg.org_data.org - - @property - def bind_vols(self) -> Iterator[Path]: - root = str(ROOT) - for app in self.services: - for vol in app.volumes: - path = self.cfg.render_all(vol.split(":", 1)[0]) - if not path.startswith(root): - continue - path = Path(path) - if path.is_file(): - continue - yield path - - # @property - # def missing_bind_vols(self) -> Iterator[Path]: - # for path in self.bind_vols: - # if path.exists(): - # continue - # yield path - - def mk_bind_vols(self) -> None: - for path in self.bind_vols: - path.mkdir(parents=True, exist_ok=True) - - def write_bind_vols(self) -> None: - self.cfg.src_paths.volume_data.write(map(str, self.bind_vols)) - - @property - def proxy_nets(self) -> Iterator[str]: - for net in self.networks.proxys: - yield self.cfg.render_all(net) - - @property - def as_rendered(self) -> str: - return self.cfg.render(self.as_template) - - def write(self) -> None: - self.cfg.dest_paths.compose_file.write(self.as_rendered) - - @override - def __call__(self, force_env:bool=False) -> None: - super(Rendered, self).__call__() - self.mk_bind_vols() - self.cfg.mk_compose_env(force_env) - self.write() - diff --git a/src/docker_compose/compose_data/__init__.py b/src/docker_compose/compose_data/__init__.py new file mode 100644 index 0000000..4f382ee --- /dev/null +++ b/src/docker_compose/compose_data/__init__.py @@ -0,0 +1,4 @@ +from docker_compose.util.replace import Replace + +DN = Replace.build_placeholder("dn", "org", "name") +FQDN = Replace.build_placeholder("fqdn", "org", "name", "service") diff --git a/src/docker_compose/compose_data/compose_yaml.py b/src/docker_compose/compose_data/compose_yaml.py new file mode 100644 index 0000000..9f0d035 --- /dev/null +++ b/src/docker_compose/compose_data/compose_yaml.py @@ -0,0 +1,12 @@ +from typing import TypedDict + +from docker_compose.compose_data.net_yaml import NetYaml +from docker_compose.compose_data.services_yaml import ServiceYamlWrite +from docker_compose.compose_data.volume_yaml import VolYaml + + +class ComposeYaml(TypedDict): + name: str + services: dict[str, ServiceYamlWrite] + networks: NetYaml | None + volumes: dict[str, VolYaml] | None diff --git a/src/docker_compose/compose_data/data.py b/src/docker_compose/compose_data/data.py new file mode 100644 index 0000000..d78ef99 --- /dev/null +++ b/src/docker_compose/compose_data/data.py @@ -0,0 +1,64 @@ +from collections.abc import Iterable, Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import Self, cast, final, override + +from docker_compose.compose_data import DN +from docker_compose.compose_data.compose_yaml import ComposeYaml +from docker_compose.compose_data.net import Net +from docker_compose.compose_data.service import Service +from docker_compose.compose_data.src_paths import SrcPaths +from docker_compose.compose_data.volume_yaml import VolYaml +from docker_compose.util.replace import Replace +from docker_compose.util.yaml_util import read_yaml, to_yaml + + +@final +@dataclass(slots=True) +class ComposeData: + name: str + services: dict[str, Service] + networks: Net + volumes: dict[str, VolYaml] + + @override + def __str__(self) -> str: + rep = Replace.format_src("name", self.name) + return rep(to_yaml(self.as_dict)) # pyright: ignore[reportArgumentType] + + @staticmethod + def get_services(paths: Iterable[Path]) -> Iterator[tuple[str, Service]]: + for path in paths: + service = Service.from_path(path) + yield service.service_name, service + + @staticmethod + def get_volumes(paths: Iterable[Path]) -> Iterator[tuple[str, VolYaml]]: + for path in paths: + yield path.stem, cast(VolYaml, cast(object, read_yaml(path))) + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls.from_src_paths(SrcPaths.from_path(path)) + + @classmethod + def from_src_paths(cls, src_paths: SrcPaths) -> Self: + services = dict(cls.get_services(src_paths.service_files)) + return cls( + src_paths.app_name, + services, + Net.from_service_list(services.values()), + dict(cls.get_volumes(src_paths.volume_files)), + ) + + @property + def as_dict(self) -> ComposeYaml: + return ComposeYaml( + name=DN.dest, + services={ + service.service_name: service.as_dict + for service in self.services.values() + }, + networks=self.networks.as_dict, + volumes=self.volumes, + ) diff --git a/src/docker_compose/compose_data/dest_paths.py b/src/docker_compose/compose_data/dest_paths.py new file mode 100644 index 0000000..4c4cca6 --- /dev/null +++ b/src/docker_compose/compose_data/dest_paths.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final + + +@final +@dataclass(frozen=True, slots=True) +class DestPaths: + compose_file: Path + bind_vol_path: Path + + @classmethod + def from_path(cls, src: Path) -> Self: + return cls( + src.joinpath("docker-compose.yml"), + src.joinpath("bind_vols.yml"), + ) diff --git a/src/docker_compose/compose_data/main.py b/src/docker_compose/compose_data/main.py new file mode 100644 index 0000000..fe9746a --- /dev/null +++ b/src/docker_compose/compose_data/main.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final, override + +from docker_compose.compose_data.data import ComposeData +from docker_compose.compose_data.dest_paths import DestPaths + + +@final +@dataclass(frozen=True, slots=True) +class Template: + compose_data: ComposeData + dest_path: DestPaths + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls( + ComposeData.from_path(path), + DestPaths.from_path(path), + ) + + def __call__(self) -> None: + with self.dest_path.compose_file.open("wt") as f: + _ = f.write(str(self.compose_data)) + + @override + def __str__(self) -> str: + return str(self.compose_data) diff --git a/src/docker_compose/compose/net.py b/src/docker_compose/compose_data/net.py similarity index 64% rename from src/docker_compose/compose/net.py rename to src/docker_compose/compose_data/net.py index 40cefdb..7a1c8e4 100644 --- a/src/docker_compose/compose/net.py +++ b/src/docker_compose/compose_data/net.py @@ -1,23 +1,27 @@ from collections.abc import Iterable, Iterator -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Self, final -from docker_compose.cfg.org import OrgData -from docker_compose.compose.net_yaml import NetArgsYaml, NetYaml -from docker_compose.compose.services import Service +from docker_compose.compose_data.net_yaml import NetArgsYaml, NetYaml +from docker_compose.compose_data.service import Service +from docker_compose.util.replace import Replace @final @dataclass(frozen=True, slots=True) class NetArgs: name: str - full_name: str = field(init=False) - external: bool = field(init=False) + full_name: str + external: bool - def __post_init__(self): - setter = super(NetArgs, self).__setattr__ - setter("full_name", f"{OrgData.org_app.dest}_{self.name}") - setter("external", self.name == "proxy") + @classmethod + def factory(cls, name: str): + f = Replace.build_placeholder("_", "org", "name").dest + return cls( + name, + f"{f}_{name}", + name == "proxy", + ) @property def as_dict(self) -> NetArgsYaml: @@ -38,6 +42,9 @@ class NetArgs: class Net: data: frozenset[NetArgs] + def __iter__(self) -> Iterator[NetArgs]: + yield from self.data + @classmethod def from_service_list(cls, args: Iterable[Service]) -> Self: return cls.from_list( @@ -46,7 +53,7 @@ class Net: @classmethod def from_list(cls, args: frozenset[str]) -> Self: - return cls(frozenset(NetArgs(arg) for arg in args)) + return cls(frozenset(NetArgs.factory(arg) for arg in args)) @property def as_dict(self) -> NetYaml: diff --git a/src/docker_compose/compose/net_yaml.py b/src/docker_compose/compose_data/net_yaml.py similarity index 100% rename from src/docker_compose/compose/net_yaml.py rename to src/docker_compose/compose_data/net_yaml.py diff --git a/src/docker_compose/compose/services.py b/src/docker_compose/compose_data/service.py similarity index 61% rename from src/docker_compose/compose/services.py rename to src/docker_compose/compose_data/service.py index 5045c86..551cf90 100644 --- a/src/docker_compose/compose/services.py +++ b/src/docker_compose/compose_data/service.py @@ -1,21 +1,19 @@ -from dataclasses import dataclass, field +from collections.abc import Callable, Iterator +from dataclasses import dataclass +from pathlib import Path from typing import Self, final, override -from docker_compose.cfg.compose_paths import ServicePath, ServiceVal -from docker_compose.cfg.org import App, Org, OrgData, Url -from docker_compose.cfg.replace import ReplaceDynamic, ReplaceStatic, ReplaceUnique -from docker_compose.compose.services_yaml import ( +import yaml + +from docker_compose.compose_data import DN, FQDN +from docker_compose.compose_data.services_yaml import ( HealthCheck, ServiceYamlRead, ServiceYamlWrite, ) -from docker_compose.util.Ts import T_Primitive - - -@final -@dataclass(frozen=True, slots=True) -class ServiceVal(ReplaceStatic): - src = ReplaceDynamic("service") +from docker_compose.util.replace import Replace +from docker_compose.util.Ts import T_Primitive, TypeYamlDict +from docker_compose.util.yaml_util import validate_typed_dict @final @@ -23,23 +21,15 @@ class ServiceVal(ReplaceStatic): class Service: _traefik_labels = frozenset( ( - f"traefik.http.routers.{OrgData.org_app.dest}.rule=Host(`{Url.src}`)", - f"traefik.http.routers.{OrgData.org_app.dest}.entrypoints=websecure", - f"traefik.docker.network={OrgData.org_app.dest}_proxy", - f"traefik.http.routers.{OrgData.org_app.dest}.tls.certresolver=le", + f"traefik.http.routers.{DN.src}.rule=Host(`{Replace.fmt('url')}`)", + f"traefik.http.routers.{DN.src}.entrypoints=websecure", + f"traefik.docker.network={DN.src}_proxy", + f"traefik.http.routers.{DN.src}.tls.certresolver=le", ) ) _sec_opts = frozenset(("no-new-privileges:true",)) - fqdn = ReplaceUnique.build_placeholder("fqdn", Org, App, ServiceVal) - - # @property - # def service_name(self) -> str: - # return self.fqdn.dest.split("_", maxsplit=3)[-1] - service_rep: ServiceVal = field(init=False) service_name: str - # service_val: ServiceVal - # fqdn: ReplaceUnique command: tuple[str, ...] entrypoint: tuple[str, ...] environment: dict[str, T_Primitive] @@ -56,17 +46,24 @@ class Service: healthcheck: HealthCheck | None ports: frozenset[str] - def __post_init__(self): - setter = super(Service, self).__setattr__ - setter("service_rep", ServiceVal(self.service_name)) - @override def __hash__(self) -> int: return hash(self.service_name) @classmethod - def from_path(cls, path: ServicePath) -> Self: - return cls.from_dict(path.path.stem, path.as_dict) + def from_path(cls, path: Path) -> Self: + with path.open("rt") as f: + return cls.from_txt(path.stem, f.read()) + + @classmethod + def from_txt(cls, name: str, data_str: str) -> Self: + for func in cls.get_pre_render_funcs(name): + data_str = func(data_str) + data_dict: TypeYamlDict = yaml.safe_load(data_str) # pyright: ignore[reportAny] + # if not isinstance(data_dict, MutableMapping): + # raise TypeError + data = validate_typed_dict(ServiceYamlRead, data_dict) + return cls.from_dict(name, data) # pyright: ignore[reportArgumentType] @classmethod def from_dict(cls, name: str, data: ServiceYamlRead) -> Self: @@ -77,6 +74,7 @@ class Service: return cls( # service_val, name, + # Replace.format_src_dest("service", name), tuple(data.get("command", ())), tuple(data.get("entrypoint", ())), data.get("environment", {}), @@ -96,6 +94,12 @@ class Service: frozenset(data.get("ports", ())), ) + @classmethod + def get_pre_render_funcs(cls, name: str) -> Iterator[Callable[[str], str]]: + yield DN + yield FQDN + yield Replace.format_src_dest("service", name) + @property def as_dict(self) -> ServiceYamlWrite: return ServiceYamlWrite( @@ -109,7 +113,7 @@ class Service: security_opt=self.security_opt, user=self.user, volumes=self.volumes, - container_name=self.fqdn.dest, + container_name=f"{DN.dest}_{self.service_name}", restart=self.restart, shm_size=self.shm_size, depends_on=self.depends_on, diff --git a/src/docker_compose/compose/services_yaml.py b/src/docker_compose/compose_data/services_yaml.py similarity index 100% rename from src/docker_compose/compose/services_yaml.py rename to src/docker_compose/compose_data/services_yaml.py diff --git a/src/docker_compose/compose_data/src_paths.py b/src/docker_compose/compose_data/src_paths.py new file mode 100644 index 0000000..8fdadf2 --- /dev/null +++ b/src/docker_compose/compose_data/src_paths.py @@ -0,0 +1,90 @@ +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final + +# class ComposeFileTemplate(Path): +# def write_dict(self, data: TypeYamlCompatibleDict) -> 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: TypeYamlCompatibleRes) -> None: +# write_yaml(data, self) + + +@final +@dataclass(frozen=True, slots=True) +class SrcPaths: + YAML_EXTS = frozenset((".yml", ".yaml")) + + app_name: str + service_files: frozenset[Path] + volume_files: frozenset[Path] + + @classmethod + def from_path(cls, src: Path) -> Self: + return cls( + src.stem, + frozenset(cls.get_yaml_files(src.joinpath("services"))), + frozenset(cls.get_yaml_files(src.joinpath("volumes"))), + ) + + @classmethod + def get_yaml_files(cls, path: Path) -> Iterator[Path]: + for service in path.iterdir(): + if service.suffix not in cls.YAML_EXTS: + continue + yield service + diff --git a/src/docker_compose/compose/volume_yaml.py b/src/docker_compose/compose_data/volume_yaml.py similarity index 100% rename from src/docker_compose/compose/volume_yaml.py rename to src/docker_compose/compose_data/volume_yaml.py diff --git a/src/docker_compose/compose/__init__.py b/src/docker_compose/env/__init__.py similarity index 100% rename from src/docker_compose/compose/__init__.py rename to src/docker_compose/env/__init__.py diff --git a/src/docker_compose/env/data.py b/src/docker_compose/env/data.py new file mode 100644 index 0000000..2e81a79 --- /dev/null +++ b/src/docker_compose/env/data.py @@ -0,0 +1,44 @@ +import re +import secrets +from collections.abc import Iterator +from dataclasses import dataclass +from functools import partial +from pathlib import Path +from typing import Self, final, override + +from docker_compose.util.replace import Replace + + +@final +@dataclass +class EnvData: + line_valid = re.compile(r"^\s*(\w+)\s*=\s*(.+)\s*$") + pswd = Replace.format_src("pswd", partial(secrets.token_urlsafe, 12)) + data: dict[str, str] + + @override + def __str__(self) -> str: + return "\n".join(sorted(map("=".join, self.with_pass))) + + @classmethod + def get_lines(cls, path: Path) -> Iterator[tuple[str, str]]: + with path.open(mode="rt") as f: + for line in f: + res = cls.line_valid.match(line) + if not res: + continue + yield res.group(1), res.group(2) + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls({k: v for k, v in cls.get_lines(path)}) + + @property + def with_pass(self) -> Iterator[tuple[str, str]]: + p = self.pswd + for k, v in self.data.items(): + if self.pswd.src not in v: + yield k, v + continue + yield k, p(v) + diff --git a/src/docker_compose/env/main.py b/src/docker_compose/env/main.py new file mode 100644 index 0000000..bb8acc1 --- /dev/null +++ b/src/docker_compose/env/main.py @@ -0,0 +1,98 @@ +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final + +from docker_compose.env.data import EnvData +from docker_compose.org.data import OrgData + + +@final +@dataclass(frozen=True, slots=True) +class Env: + env: EnvData + org_data: OrgData + + def __call__(self): + with self.org_data.dest.open("wt") as f: + _ = f.write(str(self.env)) + + @classmethod + def from_path(cls, path: Path, org: OrgData) -> Self: + return cls( + EnvData.from_path(path), + org, + ) + + @property + def org(self) -> str: + return self.org_data.org.dest + + @property + def app(self) -> str: + return self.org_data.app.dest + + +@final +@dataclass(frozen=True, slots=True) +class EnvByOrg: + data: dict[str, Env] + app: str + + def __call__(self): + for obj in self: + obj() + + def __iter__(self) -> Iterator[Env]: + yield from self.data.values() + + @classmethod + def _from_path_sub(cls, path: Path) -> Iterator[tuple[str, Env]]: + env_data = EnvData.from_path(path) + for org in OrgData.from_path(path): + env = Env(env_data, org) + yield env.org, env + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls( + dict(cls._from_path_sub(path)), + OrgData.get_app(path), + ) + + # + # @property + # def app(self) -> str: + # return self.env.org_data.app.dest + + +@final +@dataclass(frozen=True, slots=True) +class EnvByApp: + data: dict[str, EnvByOrg] + + def __iter__(self) -> Iterator[EnvByOrg]: + yield from self.data.values() + + def __call__(self) -> None: + for obj in self: + obj() + + @staticmethod + def _get_folders(path_: Path) -> Iterator[Path]: + for path in path_.iterdir(): + if not path.is_dir(): + continue + if path.stem == "traefik": + continue + yield path + + @classmethod + def _from_path_sub(cls, path_: Path) -> Iterator[tuple[str, EnvByOrg]]: + for path in cls._get_folders(path_): + by_org = EnvByOrg.from_path(path) + yield by_org.app, by_org + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls(dict(cls._from_path_sub(path))) diff --git a/README.md b/src/docker_compose/org/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from README.md rename to src/docker_compose/org/__init__.py diff --git a/src/docker_compose/org/data.py b/src/docker_compose/org/data.py new file mode 100644 index 0000000..db95b52 --- /dev/null +++ b/src/docker_compose/org/data.py @@ -0,0 +1,45 @@ +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Self, cast, final + +from docker_compose import APP_ROOT +from docker_compose.org.org_yaml import OrgDataYaml, OrgYaml +from docker_compose.util.replace import Replace +from docker_compose.util.yaml_util import read_yaml + + +@final +@dataclass(frozen=True, slots=True) +class OrgData: + app: Replace + org: Replace + url: Replace + dest: Path + + @classmethod + def from_dict(cls, app: str, org: str, data: OrgDataYaml) -> Self: + url = data.get("url") + return cls( + Replace.format_src("name", app), + Replace.format_src("org", org), + Replace.format_src( + "url", ".".join((url, "ccamper7", "net")) if url else None + ), + APP_ROOT.joinpath(org, app), + ) + + @classmethod + def get_app(cls, path: Path) -> str: + return path.stem + + @classmethod + def from_path(cls, path: Path) -> Iterator[Self]: + app = cls.get_app(path) + for org, data in cast(OrgYaml, cast(object, read_yaml(path))).items(): + yield cls.from_dict(app, org, data) + + def __iter__(self) -> Iterator[Callable[[str], str]]: + yield self.app + yield self.org + yield self.url diff --git a/src/docker_compose/cfg/org_yaml.py b/src/docker_compose/org/org_yaml.py similarity index 100% rename from src/docker_compose/cfg/org_yaml.py rename to src/docker_compose/org/org_yaml.py diff --git a/src/docker_compose/compose/volumes.py b/src/docker_compose/render/__init__.py similarity index 100% rename from src/docker_compose/compose/volumes.py rename to src/docker_compose/render/__init__.py diff --git a/src/docker_compose/render/main.py b/src/docker_compose/render/main.py new file mode 100644 index 0000000..402393c --- /dev/null +++ b/src/docker_compose/render/main.py @@ -0,0 +1,165 @@ +from collections.abc import Iterator +from dataclasses import dataclass +from itertools import chain +from pathlib import Path +from typing import Self, final, override + +from docker_compose import APP_ROOT, ROOT, TEMPLATE_ROOT +from docker_compose.compose_data.main import Template +from docker_compose.org.data import OrgData +from docker_compose.util.replace import Replace +from docker_compose.util.yaml_util import write_yaml + + +@final +@dataclass(frozen=True, slots=True) +class BindVols: + # data_rep = Replace("data", str(DATA_ROOT)) + render: "Render" + + def __call__(self): + # def mk_bind_vols(self) -> None: + for path in self: + path.mkdir(parents=True, exist_ok=True) + + def __iter__(self) -> Iterator[Path]: + root = str(ROOT) + for app in self.render.template.compose_data.services.values(): + for vol in app.volumes: + path = self.render.render(vol.split(":", 1)[0]) + if not path.startswith(root): + continue + path = Path(path) + if not path.is_dir(): + continue + yield path + + +@final +@dataclass(frozen=True, slots=True) +class Render: + data_rep = Replace("data", str(APP_ROOT)) + template: Template + org_data: OrgData + + @override + def __str__(self) -> str: + return self.render(str(self.template)) + + def __call__(self): + self.write(str(self)) + + @property + def bind_vols(self) -> BindVols: + return BindVols(self) + + def render(self, txt: str) -> str: + for func in chain((self.data_rep,), self.org_data): + txt = func(txt) + return txt + + @property + def proxy_nets(self) -> Iterator[str]: + for net in self.template.compose_data.networks: + if not net.external: + continue + yield self.render(net.full_name) + + def write(self, data: str, render:bool=False): + with self.org_data.dest.open("wt") as f: + _ = f.write(self.render(data) if render else data) + + +@final +@dataclass(frozen=True, slots=True) +class RenderByOrg: + template: Template + renders: dict[str, Render] + + def __iter__(self) -> Iterator[Render]: + yield from self.renders.values() + # yield render + + def __call__(self) -> None: + self.template() + for render in self: + render() + self.write_bind_vol_data() + + def write_bind_vol_data(self): + write_yaml(self.vols, self.template.dest_path.bind_vol_path) + + def __getitem__(self, key: str) -> Render: + return self.renders[key] + + def __bool__(self) -> bool: + return bool(self.renders) + + @staticmethod + def from_path_sub(template: Template, path: Path) -> Iterator[tuple[str, Render]]: + for org in OrgData.from_path(path): + yield org.org.dest, Render(template, org) + + @classmethod + def from_path(cls, path: Path) -> Self: + template = Template.from_path(path) + return cls( + template, + dict(cls.from_path_sub(template, path)), + ) + + @property + def app(self): + return self.template.compose_data.name + + @property + def vols(self) -> Iterator[str]: + for render in self: + for path in render.bind_vols: + yield str(path) + + @property + def proxy_nets(self) -> Iterator[str]: + for render in self: + yield from render.proxy_nets + + +@final +@dataclass(frozen=True, slots=True) +class RenderByApp: + renders: dict[str, RenderByOrg] + + def __iter__(self) -> Iterator[RenderByOrg]: + yield from self.renders.values() + + def __call__(self) -> None: + for obj in self: + obj() + + @staticmethod + def _get_folders(path_: Path) -> Iterator[Path]: + for path in path_.iterdir(): + if not path.is_dir(): + continue + if path.stem == "traefik": + continue + yield path + + @classmethod + def _from_path_sub(cls, path_: Path) -> Iterator[tuple[str, RenderByOrg]]: + for path in cls._get_folders(path_): + by_org = RenderByOrg.from_path(path) + yield by_org.app, by_org + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls(dict(cls._from_path_sub(path))) + + @classmethod + def load_all(cls) -> Self: + return cls.from_path(TEMPLATE_ROOT) + + @property + def proxy_nets(self) -> Iterator[str]: + for render in self: + yield from render.proxy_nets diff --git a/src/docker_compose/util/Ts.py b/src/docker_compose/util/Ts.py index 1f1e766..ca01524 100644 --- a/src/docker_compose/util/Ts.py +++ b/src/docker_compose/util/Ts.py @@ -17,7 +17,7 @@ type TypePrim = T_Primitive | _PrimIters | TypePrimDict # type T_TDict = MutableMapping[T_Primitive, T_Prim] -# data going to and from yaml +# data going to and from YAML type TypeYaml = T_Primitive | TypeYamlRes type TypeYamlRes = list[TypeYaml] | TypeYamlDict class TypeYamlDict(Protocol): diff --git a/src/docker_compose/util/replace.py b/src/docker_compose/util/replace.py new file mode 100644 index 0000000..74b2a28 --- /dev/null +++ b/src/docker_compose/util/replace.py @@ -0,0 +1,92 @@ +from collections.abc import Callable +from dataclasses import dataclass +from typing import Self, final + +type TypeDest = str | None | Callable[[], str] + + +@final +@dataclass(frozen=True, slots=True) +class Replace: + src: str + _dest: TypeDest + + def __call__(self, string: str) -> str: + return string.replace(self.src, self.dest) + + @classmethod + def format_src(cls, src: str, dest: TypeDest): + return cls(cls.fmt(src), dest) + + @classmethod + def format_src_dest(cls, src: str, dest: str): + return cls(cls.fmt(src), cls.fmt(dest)) + + @classmethod + def from_str(cls, src: str) -> Self: + return cls.format_src(src, src) + + @classmethod + def build_placeholder(cls, src: str, *dest: str) -> Self: + return cls.format_src( + src, + "_".join(map(cls.fmt, dest)), + ) + + @property + def dest(self) -> str: + if not self._dest: + return "" + if isinstance(self._dest, str): + return self._dest + return self._dest() + + @staticmethod + def fmt(src: str) -> str: + return f"${{_{src.upper()}}}" + + +# +# @final +# @dataclass(frozen=True, slots=True) +# class ReplaceDynamic: +# val: str +# fmt: str +# +# @classmethod +# def factory(cls, val: str): +# return cls(val, format_src(val)) +# +# def __call__(self, string: str) -> str: +# return string.replace(self.fmt, self.val) +# +# # def __str__(self) -> str: +# # return self.val if isinstance(self.val, str) else self.val.fmt +# # def build_placeholder(self, *args: "ReplaceDynamic"): +# # data = ((rep.val.upper(), rep.fmt) for rep in chain((self,), args)) +# # src: tuple[str, ...] +# # dest: tuple[str, ...] +# # src, dest = zip(*data) +# # return ReplaceUnique("_".join(src), "_".join(dest)) +# +# +# @dataclass(frozen=True, slots=True) +# class ReplaceStatic: +# src: ClassVar[ReplaceDynamic] +# _dest: None | str | Callable[[], str] +# +# def replace(self, string: str) -> str: +# return string.replace(self.src.fmt, self.dest) +# +# @property +# def dest(self) -> str: +# if not self._dest: +# return "" +# if isinstance(self._dest, str): +# return self._dest +# return self._dest() +# +# # @classmethod +# # def two_stage(cls, dest: str) -> tuple[Self, ReplaceDynamic]: +# # dest_var = ReplaceDynamic(dest) +# # return cls(dest_var.fmt), dest_var diff --git a/src/docker_compose/util/yaml_util.py b/src/docker_compose/util/yaml_util.py index 1bae131..a34685b 100644 --- a/src/docker_compose/util/yaml_util.py +++ b/src/docker_compose/util/yaml_util.py @@ -1,5 +1,5 @@ import re -from collections.abc import Iterator, MutableMapping, Sequence, Set +from collections.abc import Iterator, MutableMapping, Set from pathlib import Path from typing import ( cast, @@ -48,7 +48,7 @@ class VerboseSafeDumper(yaml.SafeDumper): def yaml_prep(data: TypeYamlCompatibleRes) -> TypeYamlCompatibleRes: if isinstance(data, MutableMapping): return dict_prep(data) - if isinstance(data, Sequence): + if isinstance(data, tuple): return tuple(list_prep(data)) res = tuple(list_prep(data)) try: @@ -110,26 +110,33 @@ def read_typed_yaml[T: TypeYamlDict]( path: Path, ) -> T: with path.open("rt") as f: - data: TypeYamlDict = yaml.safe_load(f) # pyright: ignore[reportAny] - path_to_typed(type_, data, path) - return cast(T, data) + data: T = yaml.safe_load(f) # pyright: ignore[reportAny] + return path_to_typed(type_, data, path) -def path_to_typed( - type_: type[TypeYamlDict], - data: TypeYamlDict, +def path_to_typed[T: TypeYamlDict]( + type_: type[T], + data: T, path: Path, -) -> None: +) -> T: try: - validate_typed_dict(type_, data) + return validate_typed_dict(type_, data) except (KeyError, TypeError) as e: e.add_note(f"path: {path!s}") raise e -def validate_typed_dict( - t: type[TypeYamlDict], - data: TypeYamlDict, +def validate_typed_dict[T: TypeYamlDict]( + t: type[T], + data: T, +) -> T: + _validate_typed_dict(t, data) + return cast(T, cast(object, data)) + + +def _validate_typed_dict[T: TypeYamlDict]( + t: type[T], + data: T, ) -> None: keys = frozenset(data.keys()) missing = t.__required_keys__.difference(keys) @@ -142,7 +149,7 @@ def validate_typed_dict( for key, val in data.items(): t2 = cast(type, cast(object, hints[key])) if is_typeddict(t2): - validate_typed_dict(t2, cast(TypeYamlDict, val)) + _validate_typed_dict(t2, cast(TypeYamlDict, val)) continue # try: @@ -154,7 +161,6 @@ def validate_typed_dict( e = TypeError(f"key: {key} expected *{msg}*, got *{type(val).__name__}*") e.add_note(f"key: {key!s}") raise e - # valid = isinstance(val, get_types(t2)) # except TypeError: # valid = isinstance(val, get_origin(t2))