From 6aa6c9e4dd1d358ac7a1cb3a523bddd5e448cdceca0aed4af4dcb68aae5ce592 Mon Sep 17 00:00:00 2001 From: Christian Camper Date: Thu, 18 Dec 2025 00:00:53 -0600 Subject: [PATCH] sync --- src/compose/cfg/entity.py | 33 ----- src/compose/cfg/factory.py | 30 ---- src/compose/cfg/get.py | 83 ------------ src/compose/compose/entity.py | 66 --------- src/compose/compose/factory.py | 38 ------ src/compose/compose/get.py | 26 ---- src/compose/dest_path/entity.py | 11 -- src/compose/dest_path/factory.py | 17 --- src/compose/net/entities.py | 45 ------ src/compose/net/factory.py | 21 --- src/compose/rendered/entity.py | 18 --- src/compose/rendered/factory.py | 26 ---- src/compose/rendered/get.py | 13 -- src/compose/rendered/util.py | 38 ------ src/compose/service/entity.py | 128 ------------------ src/compose/service/factory.py | 11 -- src/compose/service/get.py | 26 ---- src/compose/src_path/entity.py | 19 --- src/compose/src_path/get.py | 15 -- src/compose/template/entity.py | 40 ------ src/compose/template/factory.py | 44 ------ src/compose/template/get.py | 38 ------ src/compose/template/util.py | 5 - src/compose/template/val_obj.py | 100 -------------- src/docker_compose/Ts.py | 8 ++ src/docker_compose/__init__.py | 4 +- src/docker_compose/cfg/cfg_paths.py | 30 ++++ src/docker_compose/cfg/cfg_paths_yaml.py | 27 ++++ src/docker_compose/cfg/org_data.py | 15 ++ src/docker_compose/cfg/org_data_yaml.py | 6 + src/docker_compose/cfg/src_path.py | 30 ++++ src/docker_compose/compose/compose.py | 75 ++++++++++ src/docker_compose/compose/compose_yaml.py | 20 +++ src/docker_compose/compose/dest_path.py | 35 +++++ src/docker_compose/compose/net.py | 46 +++++++ src/docker_compose/compose/net_args.py | 28 ++++ src/docker_compose/compose/net_args_yaml.py | 6 + src/docker_compose/compose/net_yaml.py | 8 ++ src/docker_compose/compose/render.py | 39 ++++++ src/docker_compose/compose/replace_args.py | 73 ++++++++++ src/docker_compose/compose/service.py | 80 +++++++++++ .../compose/service_yaml_read.py | 60 ++++++++ .../compose/service_yaml_write.py | 15 ++ src/docker_compose/compose/val_obj.py | 81 +++++++++++ src/docker_compose/compose/volumes_yaml.py | 25 ++++ src/docker_compose/util.py | 2 +- src/docker_compose/yaml.py | 42 ++++++ 47 files changed, 752 insertions(+), 894 deletions(-) delete mode 100644 src/compose/cfg/entity.py delete mode 100644 src/compose/cfg/factory.py delete mode 100644 src/compose/cfg/get.py delete mode 100644 src/compose/compose/entity.py delete mode 100644 src/compose/compose/factory.py delete mode 100644 src/compose/compose/get.py delete mode 100644 src/compose/dest_path/entity.py delete mode 100644 src/compose/dest_path/factory.py delete mode 100644 src/compose/net/entities.py delete mode 100644 src/compose/net/factory.py delete mode 100644 src/compose/rendered/entity.py delete mode 100644 src/compose/rendered/factory.py delete mode 100644 src/compose/rendered/get.py delete mode 100644 src/compose/rendered/util.py delete mode 100644 src/compose/service/entity.py delete mode 100644 src/compose/service/factory.py delete mode 100644 src/compose/service/get.py delete mode 100644 src/compose/src_path/entity.py delete mode 100644 src/compose/src_path/get.py delete mode 100644 src/compose/template/entity.py delete mode 100644 src/compose/template/factory.py delete mode 100644 src/compose/template/get.py delete mode 100644 src/compose/template/util.py delete mode 100644 src/compose/template/val_obj.py create mode 100644 src/docker_compose/Ts.py create mode 100644 src/docker_compose/cfg/cfg_paths.py create mode 100644 src/docker_compose/cfg/cfg_paths_yaml.py create mode 100644 src/docker_compose/cfg/org_data.py create mode 100644 src/docker_compose/cfg/org_data_yaml.py create mode 100644 src/docker_compose/cfg/src_path.py create mode 100644 src/docker_compose/compose/compose.py create mode 100644 src/docker_compose/compose/compose_yaml.py create mode 100644 src/docker_compose/compose/dest_path.py create mode 100644 src/docker_compose/compose/net.py create mode 100644 src/docker_compose/compose/net_args.py create mode 100644 src/docker_compose/compose/net_args_yaml.py create mode 100644 src/docker_compose/compose/net_yaml.py create mode 100644 src/docker_compose/compose/render.py create mode 100644 src/docker_compose/compose/replace_args.py create mode 100644 src/docker_compose/compose/service.py create mode 100644 src/docker_compose/compose/service_yaml_read.py create mode 100644 src/docker_compose/compose/service_yaml_write.py create mode 100644 src/docker_compose/compose/val_obj.py create mode 100644 src/docker_compose/compose/volumes_yaml.py create mode 100644 src/docker_compose/yaml.py diff --git a/src/compose/cfg/entity.py b/src/compose/cfg/entity.py deleted file mode 100644 index 3132da0..0000000 --- a/src/compose/cfg/entity.py +++ /dev/null @@ -1,33 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path -from typing import NotRequired, TypedDict, final - -from compose.src_path.entity import SrcPaths - - -class OrgDataYaml(TypedDict): - org: str - url: NotRequired[str] - - -class CfgDataYaml(TypedDict): - services: list[str] - volumes: NotRequired[list[str]] - orgs: list[OrgDataYaml] - - -@final -@dataclass(frozen=True, slots=True) -class OrgData: - org: str - url: str | None - - -@final -@dataclass(frozen=True, slots=True) -class CfgData: - src_paths: SrcPaths - name: str - services: frozenset[Path] - volumes: frozenset[Path] | None - orgs: frozenset[OrgData] diff --git a/src/compose/cfg/factory.py b/src/compose/cfg/factory.py deleted file mode 100644 index 2358042..0000000 --- a/src/compose/cfg/factory.py +++ /dev/null @@ -1,30 +0,0 @@ -from pathlib import Path -from typing import cast - -from compose.cfg.entity import CfgData, CfgDataYaml, OrgDataYaml -from compose.cfg.get import cfg_get_orgs -from compose.src_path.entity import SrcPaths -from compose.src_path.get import src_path_get_services, src_path_get_volumes -from compose.util import read_yml, validate_typed_dict - - -def cfg_data_yml_factory(file: Path) -> CfgDataYaml: - data = cast(CfgDataYaml, read_yml(file)) - validate_typed_dict(CfgDataYaml, data, file) - - orgs_key = "orgs" - for org in data[orgs_key]: - validate_typed_dict(OrgDataYaml, org, file, (orgs_key,)) - return data - - -def cfg_data_factory(src_paths: SrcPaths) -> CfgData: - data = cfg_data_yml_factory(src_paths.cfg_file) - vols = frozenset(src_path_get_volumes(src_paths, data)) - return CfgData( - src_paths, - src_paths.cfg_dir.name, - frozenset(src_path_get_services(src_paths, data)), - vols if vols else None, - frozenset(cfg_get_orgs(data)), - ) diff --git a/src/compose/cfg/get.py b/src/compose/cfg/get.py deleted file mode 100644 index 34fb454..0000000 --- a/src/compose/cfg/get.py +++ /dev/null @@ -1,83 +0,0 @@ -from collections.abc import Iterator -from typing import cast - -from compose.cfg.entity import CfgData, CfgDataYaml, OrgData -from compose.compose.entity import VolYaml -from compose.service.entity import Service, T_Compose -from compose.service.factory import services_yaml_factory -from compose.util import get_replace_name, read_yml - - -def cfg_get_orgs(data: CfgDataYaml) -> Iterator[OrgData]: - for org_data in data["orgs"]: - yield OrgData( - org_data["org"], - org_data.get("url"), - ) - - -def _get_sec_opts( - data: T_Compose, -) -> frozenset[str]: - sec_opts = frozenset( - "no-new-privileges:true", - ) - sec = data.get("security_opt") - if not sec: - return sec_opts - return sec_opts.union(sec) - - -def _get_labels( - data: T_Compose, -) -> frozenset[str] | None: - org_name = get_replace_name("org_name") - url = get_replace_name("url") - traefik_labels = frozenset( - ( - f"traefik.http.routers.{org_name}.rule=Host(`{url}`)", - f"traefik.http.routers.{org_name}.entrypoints=websecure", - f"traefik.docker.network={org_name}_proxy", - f"traefik.http.routers.{org_name}.tls.certresolver=le", - ) - ) - labels = data.get("labels") - if not labels: - return - if "traefik.enable=true" not in labels: - return frozenset(labels) - return traefik_labels.union(labels) - - -def cfg_get_services(cfg_data: CfgData) -> Iterator[tuple[str, Service]]: - for path in cfg_data.services: - data = services_yaml_factory(path) - # yield path.stem, Service.from_dict(data) - # @classmethod - command = data.get("command") - volumes = data.get("volumes") - entry = data.get("entrypoint") - - service = Service( - tuple(command) if command else None, - get_replace_name("org_name"), - tuple(entry) if entry else None, - data.get("environment"), - data["image"], - _get_labels(data), - None, - Service.get_nets(data), - "unless-stopped", - _get_sec_opts(data), - data.get("user"), - frozenset(volumes) if volumes else None, - ) - yield path.stem, service - - -def cfg_get_volumes(cfg_data: CfgData) -> Iterator[tuple[str, VolYaml]]: - vols = cfg_data.volumes - if vols is None: - return - for path in vols: - yield path.stem, cast(VolYaml, read_yml(path)) diff --git a/src/compose/compose/entity.py b/src/compose/compose/entity.py deleted file mode 100644 index c27e4a3..0000000 --- a/src/compose/compose/entity.py +++ /dev/null @@ -1,66 +0,0 @@ -from dataclasses import asdict, dataclass -from typing import Literal, NotRequired, TypedDict, final - -from compose.cfg import T_YamlDict -from compose.cfg.entity import CfgData -from compose.net.entities import Net, NetTraefik, NetYaml -from compose.service.entity import Service, ServiceYaml, TraefikService -from compose.util import to_yaml - -type VolYaml = dict[str, T_YamlDict] - - -class ComposeYaml(TypedDict): - name: str - services: dict[str, ServiceYaml] - networks: NotRequired[NetYaml] - volumes: NotRequired[dict[str, T_YamlDict]] - - -@final -@dataclass(frozen=True, slots=True) -class Compose: - cfg: CfgData - services: dict[str, Service] - networks: Net | None - volumes: VolYaml | None - - # @classmethod - # def from_dict(cls, cfg: CfgData, data: ComposeYaml) -> Self: - # # services = dict[str, ComposeService]() - # services = dict(_get_services_dict(data)) - # # vols = frozenset(_get_volumes_dict(data)) - # return cls( - # cfg, - # services, - # services_get_networks(services.values()), - # data.get("volumes"), - # ) - - def as_yaml(self) -> str: - return to_yaml(asdict(self)) - - -# def _get_services_dict(data: ComposeYaml): -# for k, v in data["services"].items(): -# yield k, Service.from_dict(v) - - -# def _get_volumes_dict(data: ComposeYaml) -> Iterator[VolYaml]: -# vols = data.get("volumes") -# if vols is None: -# return -# for k, v in vols.items(): -# yield {k: v} - - -@final -@dataclass(frozen=True, slots=True) -class TraefikCompose: - cfg: CfgData - services: dict[Literal["traefik"], TraefikService] - networks: NetTraefik - volumes: None - - def as_yaml(self) -> str: - return to_yaml(asdict(self)) diff --git a/src/compose/compose/factory.py b/src/compose/compose/factory.py deleted file mode 100644 index f7afd06..0000000 --- a/src/compose/compose/factory.py +++ /dev/null @@ -1,38 +0,0 @@ -from compose.cfg.entity import CfgData -from compose.cfg.get import cfg_get_services, cfg_get_volumes -from compose.compose.entity import Compose, VolYaml - -# from compose.service.factory import get_traefik_service -from compose.service.get import services_get_networks - - -def compose_factory(cfg_data: CfgData) -> Compose: - services = dict(cfg_get_services(cfg_data)) - vols: VolYaml | None = dict(cfg_get_volumes(cfg_data)) - - return Compose( - cfg_data, - services, - services_get_networks(services.values()), - vols if vols else None, - ) - - -# def traefik_compose_factory(renders: Iterable[Rendered]) -> TraefikCompose: -# src_paths = src_paths_factory(TRAEFIK_PATH) -# cfg = cfg_data_factory(src_paths) -# # cfg = CfgData( -# # src_paths, -# # 'traefik', -# # frozenset((TRAEFIK_PATH.joinpath('traefik'),)), -# # None, -# # ) -# service = get_traefik_service() -# nets: NetTraefik = dict(rendered_get_nets(renders)) -# service["networks"] = list(nets.keys()) - -# return TraefikCompose( -# cfg, -# nets, -# {"traefik": TraefikService.from_dict(service)}, -# ) diff --git a/src/compose/compose/get.py b/src/compose/compose/get.py deleted file mode 100644 index 521b037..0000000 --- a/src/compose/compose/get.py +++ /dev/null @@ -1,26 +0,0 @@ -from collections.abc import Iterator - -from compose.compose.entity import Compose, TraefikCompose -from compose.util import get_replace_name - - -def compose_get_volumes(compose: Compose | TraefikCompose) -> Iterator[str]: - if isinstance(compose, TraefikCompose): - return - f = get_replace_name("DATA") - for app_data in compose.services.values(): - if app_data.volumes is None: - return - for vol in app_data.volumes: - if not vol.startswith(f): - continue - yield vol.split(":", 1)[0] - - -# def compose_get_volumes(compose: Compose | TraefikCompose): - -# return _volumes_sub(compose) -# # vols = set(_volumes_sub(compose)) -# # if not vols: -# # return None -# # return vols diff --git a/src/compose/dest_path/entity.py b/src/compose/dest_path/entity.py deleted file mode 100644 index e885d7d..0000000 --- a/src/compose/dest_path/entity.py +++ /dev/null @@ -1,11 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path -from typing import final - - -@final -@dataclass(frozen=True, slots=True) -class DestPaths: - data_dir: Path - env_file: Path - compose_file: Path diff --git a/src/compose/dest_path/factory.py b/src/compose/dest_path/factory.py deleted file mode 100644 index b7f96a1..0000000 --- a/src/compose/dest_path/factory.py +++ /dev/null @@ -1,17 +0,0 @@ -from compose.cfg import DATA_ROOT -from compose.dest_path.entity import DestPaths -from compose.template.entity import Template - - -def dest_paths_factory(template: Template) -> DestPaths: - r_args = template.replace_args - path = ( - DATA_ROOT.joinpath(template.compose.cfg.name) - if r_args is None - else r_args.data.val.path - ) - return DestPaths( - path, - path.joinpath(".env"), - path.joinpath("docker-compose.yml"), - ) diff --git a/src/compose/net/entities.py b/src/compose/net/entities.py deleted file mode 100644 index 108703a..0000000 --- a/src/compose/net/entities.py +++ /dev/null @@ -1,45 +0,0 @@ -from dataclasses import dataclass -from typing import NotRequired, TypedDict, final - -type NetTraefik = dict[str, NetArgs] - - -class NetArgsYaml(TypedDict): - name: str - external: NotRequired[bool] - - -@final -@dataclass(frozen=True, slots=True) -class NetArgs: - name: str - external: bool | None - - # @classmethod - # def from_dict(cls, data: NetArgsYaml) -> Self: - # return cls( - # data["name"], - # data.get("external"), - # ) - - -class NetYaml(TypedDict): - internal: NotRequired[NetArgsYaml] - proxy: NotRequired[NetArgsYaml] - - -@final -@dataclass(frozen=True, slots=True) -class Net: - internal: NetArgs | None - proxy: NetArgs | None - - # @classmethod - # def from_dict(cls, data: NetYaml) -> Self: - # internal = data.get("internal") - # if internal is not None: - # internal = NetArgs.from_dict(internal) - # proxy = data.get("proxy") - # if proxy is not None: - # proxy = NetArgs.from_dict(proxy) - # return cls(internal, proxy) diff --git a/src/compose/net/factory.py b/src/compose/net/factory.py deleted file mode 100644 index 5850725..0000000 --- a/src/compose/net/factory.py +++ /dev/null @@ -1,21 +0,0 @@ -from compose.net.entities import Net, NetArgs -from compose.util import get_replace_name - - -def net_args_factory(name: str, external: bool | None = None) -> NetArgs: - return NetArgs(name, external if external else None) - - -def net_factory(name: str, _internal: bool, _proxy: bool) -> Net: - return Net( - net_args_factory(name, _internal) if _internal else None, - net_args_factory(f"{name}_proxy", _proxy) if _proxy else None, - ) - - -def net_factory_re(_internal: bool, _proxy: bool) -> Net: - return net_factory( - get_replace_name("name"), - _internal, - _proxy, - ) diff --git a/src/compose/rendered/entity.py b/src/compose/rendered/entity.py deleted file mode 100644 index 133424f..0000000 --- a/src/compose/rendered/entity.py +++ /dev/null @@ -1,18 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path -from typing import final - -from compose.dest_path.entity import DestPaths -from compose.src_path.entity import SrcPaths -from compose.template.entity import Template - - -@final -@dataclass(frozen=True, slots=True) -class Rendered: - template: Template - src_paths: SrcPaths - dest_paths: DestPaths - volumes: frozenset[Path] | None - proxy_net: str | None - data: str diff --git a/src/compose/rendered/factory.py b/src/compose/rendered/factory.py deleted file mode 100644 index d2f981b..0000000 --- a/src/compose/rendered/factory.py +++ /dev/null @@ -1,26 +0,0 @@ -from functools import reduce - -from compose.dest_path.factory import dest_paths_factory -from compose.rendered.entity import Rendered -from compose.template.entity import Template -from compose.template.get import template_get_proxy, template_get_vols -from compose.template.util import replace - - -def rendered_factory(template: Template) -> Rendered: - yml = template.compose.as_yaml() - if template.replace_args is not None: - yml = reduce( - lambda s, f: replace(f, s), - template.replace_args, - yml, - ) - vols = frozenset(template_get_vols(template)) - return Rendered( - template, - template.compose.cfg.src_paths, - dest_paths_factory(template), - vols if vols else None, - template_get_proxy(template), - yml, - ) diff --git a/src/compose/rendered/get.py b/src/compose/rendered/get.py deleted file mode 100644 index 3494c97..0000000 --- a/src/compose/rendered/get.py +++ /dev/null @@ -1,13 +0,0 @@ -from collections.abc import Iterable, Iterator - -from compose.net.entities import NetArgs -from compose.net.factory import net_args_factory -from compose.rendered.entity import Rendered - - -def rendered_get_nets(renders: Iterable[Rendered]) -> Iterator[tuple[str, NetArgs]]: - for render in renders: - net = render.proxy_net - if net is None: - continue - yield net, net_args_factory(f"{net}_proxy") diff --git a/src/compose/rendered/util.py b/src/compose/rendered/util.py deleted file mode 100644 index 9adee6c..0000000 --- a/src/compose/rendered/util.py +++ /dev/null @@ -1,38 +0,0 @@ -from pathlib import Path -from shutil import copyfile - -from compose.rendered.entity import Rendered - - -def _mk_dir(path: Path) -> None: - if path.exists(): - return - path.mkdir(parents=True) - - -def _mk_compose_dir(rendered: Rendered) -> None: - _mk_dir(rendered.dest_paths.data_dir) - vols = rendered.volumes - if vols is None: - return - for vol in vols: - _mk_dir(vol) - - -def _mk_compose_env(rendered: Rendered) -> None: - src = rendered.src_paths.env_file - dest = rendered.dest_paths.env_file - if src.exists() and not dest.exists(): - _ = copyfile(src, dest) - - -def write_raw(path: Path, data: str) -> None: - with path.open("wt") as f: - _ = f.write(data) - - -def write(rendered: Rendered) -> None: - funcs = (_mk_compose_dir, _mk_compose_env) - for func in funcs: - func(rendered) - write_raw(rendered.dest_paths.compose_file, rendered.data) diff --git a/src/compose/service/entity.py b/src/compose/service/entity.py deleted file mode 100644 index 5a47acc..0000000 --- a/src/compose/service/entity.py +++ /dev/null @@ -1,128 +0,0 @@ -from abc import ABCMeta, abstractmethod -from dataclasses import dataclass -from typing import Literal, NotRequired, TypedDict, TypeVar, overload, override - -from compose.cfg import T_Primitive - -type T_NetAbc = str | Literal["proxy", "internal"] -TCo_NetABC = TypeVar("TCo_NetABC", bound=T_NetAbc, covariant=True) - - -class ServiceYamlAbc[T_net: T_NetAbc](TypedDict): - command: NotRequired[list[str]] - container_name: NotRequired[str] - entrypoint: NotRequired[list[str]] - environment: NotRequired[dict[str, T_Primitive]] - image: str - labels: NotRequired[list[str]] - logging: NotRequired[dict[str, str]] - networks: NotRequired[list[T_net]] - restart: NotRequired[str] - security_opt: NotRequired[list[str]] - user: NotRequired[str] - volumes: NotRequired[list[str]] - - -# TCo_ServiceYaml = TypeVar( -# "TCo_ServiceYaml", -# bound=ServiceYamlAbc[T_NetAbc], -# covariant=True, -# ) - - -class ServiceYaml(ServiceYamlAbc[Literal["proxy", "internal"]]): - pass - - -class TraefikServiceYaml(ServiceYamlAbc[str]): - pass - - -type T_Compose = ServiceYaml | TraefikServiceYaml - - -@dataclass(frozen=True, slots=True) -class ServiceAbc[T_net: T_NetAbc, T_Yaml: T_Compose](metaclass=ABCMeta): - command: tuple[str, ...] | None - container_name: str - entrypoint: tuple[str, ...] | None - environment: dict[str, T_Primitive] | None - image: str - labels: frozenset[str] | None - logging: dict[str, str] | None - networks: frozenset[T_net] | None - restart: str - security_opt: frozenset[str] - user: str | None - volumes: frozenset[str] | None - - # @classmethod - # def from_dict(cls, data: T_Yaml) -> Self: - # command = data.get("command") - # volumes = data.get("volumes") - # entry = data.get("entrypoint") - # name = data.get("container_name") - # if name is None: - # raise KeyError - # return cls( - # tuple(command) if command else None, - # name, - # tuple(entry) if entry else None, - # data.get("environment"), - # data["image"], - # _get_labels(data), - # None, - # cls.get_nets(data), - # "unless-stopped", - # _get_sec_opts(data), - # data.get("user"), - # frozenset(volumes) if volumes else None, - # ) - - # def is_valid(self) -> bool: - # attrs = (self.container_name, self.restart, self.logging) - # for attr in attrs: - # if attr is None: - # return False - # return True - - @staticmethod - @abstractmethod - def get_nets(data: T_Yaml) -> frozenset[T_net] | None: - pass - # return self._get_nets(data) - - -class Service(ServiceAbc[Literal["proxy", "internal"], ServiceYaml]): - @override - @staticmethod - def get_nets(data: ServiceYaml) -> frozenset[Literal["proxy", "internal"]] | None: - return _get_nets(data) - - -class TraefikService(ServiceAbc[str, TraefikServiceYaml]): - @override - @staticmethod - def get_nets(data: TraefikServiceYaml) -> frozenset[str] | None: - return _get_nets(data) - - -@overload -def _get_nets( - data: ServiceYaml, -) -> frozenset[Literal["proxy", "internal"]] | None: - pass - - -@overload -def _get_nets(data: TraefikServiceYaml) -> frozenset[str] | None: - pass - - -def _get_nets( - data: ServiceYaml | TraefikServiceYaml, -) -> frozenset[str] | frozenset[Literal["proxy", "internal"]] | None: - nets = data.get("networks") - if nets is None: - return - return frozenset(nets) diff --git a/src/compose/service/factory.py b/src/compose/service/factory.py deleted file mode 100644 index 78d1dd6..0000000 --- a/src/compose/service/factory.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path -from typing import cast - -from compose.service.entity import ServiceYaml -from compose.util import read_yml, validate_typed_dict - - -def services_yaml_factory(path: Path): - data = cast(ServiceYaml, read_yml(path)) - validate_typed_dict(ServiceYaml, data, path) - return data diff --git a/src/compose/service/get.py b/src/compose/service/get.py deleted file mode 100644 index c0469f1..0000000 --- a/src/compose/service/get.py +++ /dev/null @@ -1,26 +0,0 @@ -from collections.abc import Iterable, Iterator -from typing import Literal - -from compose.net.entities import Net -from compose.net.factory import net_factory_re -from compose.service.entity import Service - - -def _networks_sub( - services: Iterable[Service], -) -> Iterator[Literal["proxy", "internal"]]: - for app_data in services: - networks = app_data.networks - if networks is None: - continue - yield from networks - - -def services_get_networks(services: Iterable[Service]) -> Net | None: - networks = frozenset(_networks_sub(services)) - if not networks: - return None - return net_factory_re( - "internal" in networks, - "proxy" in networks, - ) diff --git a/src/compose/src_path/entity.py b/src/compose/src_path/entity.py deleted file mode 100644 index 115a12c..0000000 --- a/src/compose/src_path/entity.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path -from typing import final - - -@final -@dataclass(frozen=True, slots=True) -class SrcPaths: - cfg_dir: Path - cfg_file: Path - env_file: Path - - -def src_paths_factory(src: Path) -> SrcPaths: - return SrcPaths( - cfg_dir=src, - cfg_file=src.joinpath("cfg.yml"), - env_file=src.joinpath(".env"), - ) diff --git a/src/compose/src_path/get.py b/src/compose/src_path/get.py deleted file mode 100644 index 27ebac6..0000000 --- a/src/compose/src_path/get.py +++ /dev/null @@ -1,15 +0,0 @@ -from compose.cfg.entity import CfgDataYaml -from compose.src_path.entity import SrcPaths - - -def src_path_get_services(src_paths: SrcPaths, data: CfgDataYaml): - for path in data["services"]: - yield src_paths.cfg_dir.joinpath(path) - - -def src_path_get_volumes(src_paths: SrcPaths, data: CfgDataYaml): - vols = data.get("volumes") - if vols is None: - return - for path in vols: - yield src_paths.cfg_dir.joinpath(path) diff --git a/src/compose/template/entity.py b/src/compose/template/entity.py deleted file mode 100644 index c596cfb..0000000 --- a/src/compose/template/entity.py +++ /dev/null @@ -1,40 +0,0 @@ -from collections.abc import Iterator -from dataclasses import dataclass -from typing import final - -from compose.compose.entity import Compose, TraefikCompose -from compose.template.val_obj import ( - DataDir, - NameVal, - OrgVal, - RecordCls, - T_RecordCls, - Url, -) - - -@final -@dataclass(frozen=True, slots=True) -class ReplaceArgs: - org: RecordCls[OrgVal] - name: RecordCls[NameVal] - org_name: RecordCls[NameVal] - data: RecordCls[DataDir] - url: RecordCls[Url] - - def __iter__(self) -> Iterator[T_RecordCls]: - yield self.org - yield self.name - yield self.org_name - yield self.data - yield self.url - - -@final -@dataclass(frozen=True, slots=True) -class Template: - compose: Compose | TraefikCompose - replace_args: ReplaceArgs | None - # dest_paths: DestPaths - volumes: frozenset[str] | None - # proxy_net: str | None diff --git a/src/compose/template/factory.py b/src/compose/template/factory.py deleted file mode 100644 index 5b99cef..0000000 --- a/src/compose/template/factory.py +++ /dev/null @@ -1,44 +0,0 @@ -from collections.abc import Iterator - -from compose.cfg import DATA_ROOT -from compose.cfg.entity import CfgData, OrgData -from compose.compose.entity import Compose, TraefikCompose -from compose.compose.get import compose_get_volumes -from compose.template.entity import ReplaceArgs, Template -from compose.template.val_obj import DataDir, NameVal, OrgVal, Record, Url - - -def replace_args_factory(cfg_data: CfgData, org_data: OrgData) -> ReplaceArgs: - _org = OrgVal(org_data.org) - _name = NameVal(cfg_data.name) - org_name = ( - NameVal(f"{_org.to_str()}_{_name.to_str()}") if _org.is_valid() else _name - ) - data_dir = DATA_ROOT.joinpath(_org.to_str(), _name.to_str()) - - return ReplaceArgs( - Record("org", _org), - Record("name", _name), - Record("org_name", org_name), - Record("data", DataDir(data_dir)), - Record("url", Url(org_data.url)), - ) - - -def template_factory(compose: Compose | TraefikCompose) -> Iterator[Template]: - cfg_data = compose.cfg - vols = frozenset(compose_get_volumes(compose)) - if not vols: - vols = None - - for org_data in cfg_data.orgs: - args = replace_args_factory( - cfg_data, - org_data, - ) - - yield Template( - compose, - args, - vols, - ) diff --git a/src/compose/template/get.py b/src/compose/template/get.py deleted file mode 100644 index f6ab888..0000000 --- a/src/compose/template/get.py +++ /dev/null @@ -1,38 +0,0 @@ -from collections.abc import Iterable -from functools import partial -from pathlib import Path - -from compose.net.entities import Net -from compose.template.entity import Template -from compose.template.util import replace - - -def template_get_vols(template: Template) -> Iterable[Path]: - def _lambda(x: str) -> str: - return x - - vols = template.volumes - if not vols: - return - r_args = template.replace_args - re = _lambda if r_args is None else partial(replace, r_args.data) - for vol in vols: - yield Path(re(vol)) - - -def template_get_proxy(template: Template) -> str | None: - proxy = template.compose.networks - if proxy is None: - return - if not isinstance(proxy, Net): - return - if proxy.proxy is None: - return - if proxy.internal is None: - net = proxy.proxy.name.replace("_proxy", "") - else: - net = proxy.internal.name - r_args = template.replace_args - if r_args is None: - return net - return replace(r_args.name, net) diff --git a/src/compose/template/util.py b/src/compose/template/util.py deleted file mode 100644 index d32269b..0000000 --- a/src/compose/template/util.py +++ /dev/null @@ -1,5 +0,0 @@ -from compose.template.val_obj import T_RecordCls - - -def replace(record: T_RecordCls, string: str) -> str: - return str.replace(string, record.name, record.val.to_str()) diff --git a/src/compose/template/val_obj.py b/src/compose/template/val_obj.py deleted file mode 100644 index 6e4a35b..0000000 --- a/src/compose/template/val_obj.py +++ /dev/null @@ -1,100 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path -from typing import Protocol, final - -from compose.util import get_replace_name - -# class RecordVal(ABC): -# @abstractmethod -# def to_str(self) -> str: -# pass - - -@final -class RecordVal(Protocol): - def to_str(self) -> str: ... - - -# TCo_RecordVal = TypeVar( -# "TCo_RecordVal", -# bound=RecordVal, -# covariant=True, -# ) -# TCon_RecordVal = TypeVar( -# "TCon_RecordVal", -# bound=RecordVal, -# contravariant=True, -# ) -@final -class T_RecordCls(Protocol): - # name: str - # val: RecordVal - - @property - def name(self) -> str: ... - - @property - def val(self) -> RecordVal: ... - - -@final -@dataclass(frozen=True, slots=True) -class RecordCls[T: RecordVal]: - name: str - val: T - - # @final - # class RecordClsProto(Protocol): - # name:str - # val: RecordVal - - -# def replace(self:RecordCls[TCo_RecordVal], string: str) -> str: -# return str.replace(string, self.name, self.val.to_str()) - - -@final -@dataclass(frozen=True, slots=True) -class OrgVal: - val: str | None - - def to_str(self) -> str: - if self.val is None: - return "personal" - return self.val - - def is_valid(self) -> bool: - return self.val is not None - - -@final -@dataclass(frozen=True, slots=True) -class NameVal: - val: str - - def to_str(self) -> str: - return self.val - - -@final -@dataclass(frozen=True, slots=True) -class DataDir: - path: Path - - def to_str(self) -> str: - return str(self.path) - - -@final -@dataclass(frozen=True, slots=True) -class Url: - sub_url: str | None - - def to_str(self) -> str: - if self.sub_url is None: - return "" - return ".".join([self.sub_url, "ccamper7", "net"]) - - -def Record[T: RecordVal](name: str, val: T) -> RecordCls[T]: - return RecordCls(get_replace_name(name), val) diff --git a/src/docker_compose/Ts.py b/src/docker_compose/Ts.py new file mode 100644 index 0000000..8154bb0 --- /dev/null +++ b/src/docker_compose/Ts.py @@ -0,0 +1,8 @@ +from collections.abc import Mapping + +type nested_list = list[str | nested_list] +type T_Primitive = bool | int | str +type T_PrimVal = T_Primitive | list[T_Primitive] | T_PrimDict +type T_PrimDict = Mapping[T_Primitive, T_PrimVal] +type T_YamlVals = T_Primitive | list[T_Primitive | T_YamlDict] | T_YamlDict +type T_YamlDict = Mapping[str, T_YamlVals] diff --git a/src/docker_compose/__init__.py b/src/docker_compose/__init__.py index 1429728..747a8e5 100644 --- a/src/docker_compose/__init__.py +++ b/src/docker_compose/__init__.py @@ -1,7 +1,7 @@ from collections.abc import Iterable, Iterator -from compose.cfg import CFG_ROOT, TRAEFIK_PATH -from compose.compose.render import Rendered +from docker_compose.cfg import CFG_ROOT, TRAEFIK_PATH +from docker_compose.compose.render import Rendered def load_all() -> Iterable[Rendered]: diff --git a/src/docker_compose/cfg/cfg_paths.py b/src/docker_compose/cfg/cfg_paths.py new file mode 100644 index 0000000..6b6d30f --- /dev/null +++ b/src/docker_compose/cfg/cfg_paths.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final + +from docker_compose.cfg.cfg_paths_yaml import CfgYaml +from docker_compose.cfg.org_data import OrgData +from docker_compose.cfg.src_path import SrcPaths + + +@final +@dataclass(frozen=True, slots=True) +class CfgData: + src_paths: SrcPaths + name: str + services: frozenset[Path] + volumes: frozenset[Path] | None + org_data: frozenset[OrgData] + + @classmethod + def from_src_paths(cls, src_paths: SrcPaths) -> Self: + cfg_yaml = CfgYaml.from_src_paths(src_paths) + cfg_root = src_paths.cfg_dir.joinpath + vols = cfg_yaml.data.get("volumes") + return cls( + src_paths, + src_paths.cfg_dir.name, + frozenset(cfg_root(path) for path in cfg_yaml.data["services"]), + frozenset(cfg_root(path) for path in vols) if vols else None, + frozenset(cfg_yaml.orgs_data), + ) diff --git a/src/docker_compose/cfg/cfg_paths_yaml.py b/src/docker_compose/cfg/cfg_paths_yaml.py new file mode 100644 index 0000000..9786513 --- /dev/null +++ b/src/docker_compose/cfg/cfg_paths_yaml.py @@ -0,0 +1,27 @@ +from collections.abc import Iterator +from dataclasses import dataclass +from typing import NotRequired, Self, TypedDict, final + +from docker_compose.cfg.org_data import OrgData +from docker_compose.cfg.org_data_yaml import OrgDataYaml +from docker_compose.cfg.src_path import SrcPaths +from docker_compose.yaml import YamlWrapper + + +class CfgYamlData(TypedDict): + services: list[str] + volumes: NotRequired[list[str]] + orgs: list[OrgDataYaml] + + +@final +@dataclass(frozen=True, slots=True) +class CfgYaml(YamlWrapper[CfgYamlData]): + @classmethod + def from_src_paths(cls, src_paths: SrcPaths) -> Self: + return cls.from_path(src_paths.cfg_file) + + @property + def orgs_data(self) -> Iterator[OrgData]: + for org in self.data["orgs"]: + yield OrgData.from_dict(org) diff --git a/src/docker_compose/cfg/org_data.py b/src/docker_compose/cfg/org_data.py new file mode 100644 index 0000000..bca6b26 --- /dev/null +++ b/src/docker_compose/cfg/org_data.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import Self, final + +from docker_compose.cfg.org_data_yaml import OrgDataYaml + + +@final +@dataclass(frozen=True, slots=True) +class OrgData: + org: str + url: str | None + + @classmethod + def from_dict(cls, data: OrgDataYaml) -> Self: + return cls(data["org"], data.get("url")) diff --git a/src/docker_compose/cfg/org_data_yaml.py b/src/docker_compose/cfg/org_data_yaml.py new file mode 100644 index 0000000..b9a6a9f --- /dev/null +++ b/src/docker_compose/cfg/org_data_yaml.py @@ -0,0 +1,6 @@ +from typing import NotRequired, TypedDict + + +class OrgDataYaml(TypedDict): + org: str + url: NotRequired[str] diff --git a/src/docker_compose/cfg/src_path.py b/src/docker_compose/cfg/src_path.py new file mode 100644 index 0000000..0ebdb66 --- /dev/null +++ b/src/docker_compose/cfg/src_path.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final + + +@final +@dataclass(frozen=True, slots=True) +class SrcPaths: + cfg_dir: Path + cfg_file: Path + env_file: Path + + @classmethod + def from_path(cls, src: Path) -> Self: + return cls( + cfg_dir=src, + cfg_file=src.joinpath("cfg.yml"), + env_file=src.joinpath(".env"), + ) + + # def src_path_get_services(src_paths: SrcPaths, data: CfgDataYaml): + # for path in data["services"]: + # yield src_paths.cfg_dir.joinpath(path) + + # def src_path_get_volumes(src_paths: SrcPaths, data: CfgDataYaml): + # vols = data.get("volumes") + # if vols is None: + # return + # for path in vols: + # yield src_paths.cfg_dir.joinpath(path) diff --git a/src/docker_compose/compose/compose.py b/src/docker_compose/compose/compose.py new file mode 100644 index 0000000..39c9ffa --- /dev/null +++ b/src/docker_compose/compose/compose.py @@ -0,0 +1,75 @@ +from abc import ABCMeta +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import Self + +from docker_compose.cfg.cfg_paths import CfgData +from docker_compose.cfg.src_path import SrcPaths +from docker_compose.compose.compose_yaml import ComposeYaml, ComposeYamlData +from docker_compose.compose.net import Net +from docker_compose.compose.replace_args import ReplaceArgs +from docker_compose.compose.service import Service +from docker_compose.compose.volumes_yaml import VolYaml + + +@dataclass(frozen=True, slots=True) +class Compose(metaclass=ABCMeta): + cfg: CfgData + services: dict[str, Service] + networks: Net | None + volumes: dict[str, VolYaml] | None + replace_args: frozenset[ReplaceArgs] + + @classmethod + def from_path(cls, path: Path): + return cls.from_src_path(SrcPaths.from_path(path)) + + @classmethod + def from_src_path(cls, src_paths: SrcPaths) -> Self: + return cls.from_cfg(CfgData.from_src_paths(src_paths)) + + @classmethod + def from_cfg(cls, cfg_data: CfgData) -> Self: + services = {path.stem: Service.from_path(path) for path in cfg_data.services} + vols = {path.stem: VolYaml.from_path(path) for path in cfg_data.services} + nets = Net.from_service_list(services.values()) + return cls( + cfg_data, + services, + nets if nets else None, + vols if vols else None, + frozenset( + ReplaceArgs.from_cfg_data(cfg_data, org) for org in cfg_data.org_data + ), + ) + + @property + def as_dict(self) -> ComposeYaml: + data = ComposeYamlData( + name=self.cfg.name, + services={k: v.as_dict.data for k, v in self.services.items()}, + ) + if self.networks: + data["networks"] = self.networks.as_dict + if self.volumes: + data["volumes"] = {k: v.data for k, v in self.volumes.items()} + + return ComposeYaml(data) + + @property + def as_yaml(self) -> str: + return self.as_dict.as_yaml + + @property + def proxys(self) -> Iterator[str]: + proxy = self.networks + if proxy is None: + return + for net in proxy.data.values(): + if not net.is_proxy: + return + yield net.name + + # nets = frozenset(net.name for net in proxy.data.values() if net.is_proxy) + # return nets if nets else None diff --git a/src/docker_compose/compose/compose_yaml.py b/src/docker_compose/compose/compose_yaml.py new file mode 100644 index 0000000..c7efbff --- /dev/null +++ b/src/docker_compose/compose/compose_yaml.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import NotRequired, TypedDict, final + +from docker_compose.compose.net_yaml import NetYaml +from docker_compose.compose.service_yaml_write import ServiceYamlWriteData +from docker_compose.compose.volumes_yaml import VolYamlData +from docker_compose.yaml import YamlWrapper + + +class ComposeYamlData(TypedDict): + name: str + services: dict[str, ServiceYamlWriteData] + networks: NotRequired[NetYaml] + volumes: NotRequired[dict[str, VolYamlData]] + + +@final +@dataclass(frozen=True, slots=True) +class ComposeYaml(YamlWrapper[ComposeYamlData]): + pass diff --git a/src/docker_compose/compose/dest_path.py b/src/docker_compose/compose/dest_path.py new file mode 100644 index 0000000..ae51df2 --- /dev/null +++ b/src/docker_compose/compose/dest_path.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final + + +@final +@dataclass(frozen=True, slots=True) +class DestPaths: + data_dir: Path + env_file: Path + compose_file: Path + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls( + path, + path.joinpath(".env"), + path.joinpath("docker-docker_compose.yml"), + ) + + # @staticmethod + # def _mk_dir(path: Path) -> None: + # if path.exists(): + # return + # path.mkdir(parents=True) + + def mk_compose_dir(self) -> None: + if self.data_dir.exists(): + return + self.data_dir.mkdir(parents=True) + # vols = self.bind_vols + # if vols is None: + # return + # for vol in vols: + # _mk_dir(vol) diff --git a/src/docker_compose/compose/net.py b/src/docker_compose/compose/net.py new file mode 100644 index 0000000..15d12f0 --- /dev/null +++ b/src/docker_compose/compose/net.py @@ -0,0 +1,46 @@ +from collections.abc import Iterable +from dataclasses import dataclass +from typing import final + +from docker_compose.compose.net_args import NetArgs +from docker_compose.compose.net_yaml import NetYaml +from docker_compose.compose.service import Service + +# @final +# @dataclass(frozen=True, slots=True) +# class Net: +# internal: NetArgs | None +# proxy: NetArgs | None + +# @property +# def as_dict(self) -> NetYaml: +# yaml_dict = NetYaml() +# if self.internal is not None: +# yaml_dict["internal"] = self.internal.as_dict + +# if self.proxy is not None: +# yaml_dict["proxy"] = self.proxy.as_dict +# return yaml_dict + + +@final +@dataclass +class Net: + data: dict[str, NetArgs] + + @classmethod + def from_service_list(cls, args: Iterable[Service]): + return cls.from_list( + {net for service in args if service.networks for net in service.networks} + ) + + @classmethod + def from_list(cls, args: Iterable[str]): + return cls({net: NetArgs(net, NetArgs.is_proxy_check(net)) for net in args}) + + @property + def as_dict(self) -> NetYaml: + return {name: net_args.as_dict for name, net_args in self.data.items()} + + def __bool__(self) -> bool: + return bool(self.data) diff --git a/src/docker_compose/compose/net_args.py b/src/docker_compose/compose/net_args.py new file mode 100644 index 0000000..7085d34 --- /dev/null +++ b/src/docker_compose/compose/net_args.py @@ -0,0 +1,28 @@ +from dataclasses import dataclass +from typing import final + +from docker_compose.compose.net_args_yaml import NetArgsYaml + + +@final +@dataclass(frozen=True, slots=True) +class NetArgs: + name: str + external: bool | None + + @property + def as_dict(self) -> NetArgsYaml: + yaml_dict = NetArgsYaml( + name=self.name, + ) + if self.external is not None: + yaml_dict["external"] = self.external + return yaml_dict + + @staticmethod + def is_proxy_check(name: str) -> bool: + return name.endswith("proxy") + + @property + def is_proxy(self) -> bool: + return self.is_proxy_check(self.name) diff --git a/src/docker_compose/compose/net_args_yaml.py b/src/docker_compose/compose/net_args_yaml.py new file mode 100644 index 0000000..1309476 --- /dev/null +++ b/src/docker_compose/compose/net_args_yaml.py @@ -0,0 +1,6 @@ +from typing import NotRequired, TypedDict + + +class NetArgsYaml(TypedDict): + name: str + external: NotRequired[bool] diff --git a/src/docker_compose/compose/net_yaml.py b/src/docker_compose/compose/net_yaml.py new file mode 100644 index 0000000..ad8f446 --- /dev/null +++ b/src/docker_compose/compose/net_yaml.py @@ -0,0 +1,8 @@ +from docker_compose.compose.net_args_yaml import NetArgsYaml + +# class NetYaml(TypedDict): +# internal: NotRequired[NetArgsYaml] +# proxy: NotRequired[NetArgsYaml] + + +type NetYaml = dict[str, NetArgsYaml] diff --git a/src/docker_compose/compose/render.py b/src/docker_compose/compose/render.py new file mode 100644 index 0000000..b59c85c --- /dev/null +++ b/src/docker_compose/compose/render.py @@ -0,0 +1,39 @@ +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import final + +from docker_compose.compose.compose import Compose +from docker_compose.compose.replace_args import ReplaceArgs + + +@final +@dataclass(frozen=True, slots=True) +class Rendered(Compose): + def mk_bind_vols(self) -> None: + for app_data in self.services.values(): + if app_data.volumes is None: + continue + for vol in app_data.volumes: + for arg in self.replace_args: + path = arg.render_yaml(vol.split(":", 1)[0]) + if not path.startswith("/"): + continue + path = Path(path) + if path.exists(): + continue + path.mkdir(parents=True) + + @property + def proxy_nets(self) -> Iterator[str]: + for net in self.proxys: + for re in self.replace_args: + yield re.org_name.replace(net) + + def write_all(self) -> None: + self.mk_bind_vols() + for arg in self.replace_args: + arg.write_yaml(self.as_yaml) + + def write(self, args: ReplaceArgs) -> None: + args.write_yaml(self.as_yaml) diff --git a/src/docker_compose/compose/replace_args.py b/src/docker_compose/compose/replace_args.py new file mode 100644 index 0000000..1aa4705 --- /dev/null +++ b/src/docker_compose/compose/replace_args.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass +from functools import reduce +from shutil import copyfile +from typing import Self, final + +from docker_compose.cfg import DATA_ROOT +from docker_compose.cfg.cfg_paths import CfgData +from docker_compose.cfg.org_data import OrgData +from docker_compose.compose.dest_path import DestPaths +from docker_compose.compose.val_obj import ( + DataDir, + NameVal, + OrgVal, + Record, + Url, +) + + +@final +@dataclass(frozen=True, slots=True) +class ReplaceArgs: + cfg: CfgData + org: Record[OrgVal] + name: Record[NameVal] + org_name: Record[NameVal] + data: Record[DataDir] + url: Record[Url] + dest_paths: DestPaths + + # noinspection PyMissingTypeHints + def __iter__(self): + yield self.org + yield self.name + yield self.org_name + yield self.data + yield self.url + + @classmethod + def from_cfg_data(cls, cfg_data: CfgData, org_data: OrgData) -> Self: + _org = OrgVal(org_data.org) + _name = NameVal(cfg_data.name) + org_name = NameVal(f"{_org.str}_{_name.str}") if _org.is_valid() else _name + data_dir = DATA_ROOT.joinpath(_org.str, _name.str) + + return cls( + cfg_data, + Record("org", _org), + Record("name", _name), + Record("org_name", org_name), + Record("data", DataDir(data_dir)), + Record("url", Url(org_data.url)), + DestPaths.from_path(data_dir), + ) + + def mk_compose_env(self) -> None: + src = self.cfg.src_paths.env_file + dest = self.dest_paths.env_file + if src.exists() and not dest.exists(): + _ = copyfile(src, dest) + + def render_yaml(self, yaml: str) -> str: + return reduce(lambda s, f: f.replace(s), self, yaml) + + def write_yaml(self, yaml: str) -> None: + self.dest_paths.mk_compose_dir() + with self.dest_paths.compose_file.open("wt") as f: + _ = f.write(self.render_yaml(yaml)) + + # def mk_vol_dir(self, path: str): + # p = Path(self.render_yaml(path)) + # if p.exists(): + # return + # p.mkdir(parents=True) diff --git a/src/docker_compose/compose/service.py b/src/docker_compose/compose/service.py new file mode 100644 index 0000000..f06edd2 --- /dev/null +++ b/src/docker_compose/compose/service.py @@ -0,0 +1,80 @@ +from abc import ABCMeta +from dataclasses import dataclass +from pathlib import Path +from typing import Self, final + +from docker_compose.compose.service_yaml_read import ( + ServiceYamlRead, +) +from docker_compose.compose.service_yaml_write import ( + ServiceYamlWrite, + ServiceYamlWriteData, +) +from docker_compose.compose.val_obj import Record +from docker_compose.Ts import T_Primitive + + +@final +@dataclass(frozen=True, slots=True) +class Service(metaclass=ABCMeta): + command: tuple[str, ...] | None + container_name: str + entrypoint: tuple[str, ...] | None + environment: dict[str, T_Primitive] | None + image: str + labels: frozenset[str] | None + logging: dict[str, str] | None + networks: frozenset[str] | None + restart: str + security_opt: frozenset[str] + user: str | None + volumes: frozenset[str] | None + + @classmethod + def from_path(cls, path: Path) -> Self: + return cls.from_dict(ServiceYamlRead.from_path(path)) + + @classmethod + def from_dict(cls, data: ServiceYamlRead): + command = data.data.get("command") + entrypoint = data.data.get("entrypoint") + volumes = data.data.get("volumes") + nets = data.data.get("networks") + return cls( + None if not command else tuple(command), + Record.get_replace_name("org_name"), + tuple(entrypoint) if entrypoint else None, + data.data.get("environment"), + data.data["image"], + data.labels, + data.data.get("logging"), + frozenset(nets) if nets else None, + "unless-stopped", + data.sec_opts, + data.data.get("user"), + frozenset(volumes) if volumes else None, + ) + + @property + def as_dict(self) -> ServiceYamlWrite: + data = ServiceYamlWriteData( + container_name=self.container_name, + image=self.image, + restart=self.restart, + security_opt=sorted(self.security_opt), + ) + if self.command is not None: + data["command"] = list(self.command) + if self.entrypoint is not None: + data["entrypoint"] = list(self.entrypoint) + if self.environment is not None: + data["environment"] = self.environment + if self.labels is not None: + data["labels"] = sorted(self.labels) + if self.logging is not None: + data["logging"] = self.logging + if self.user is not None: + data["user"] = self.user + if self.volumes is not None: + data["volumes"] = sorted(self.volumes) + return ServiceYamlWrite(data) diff --git a/src/docker_compose/compose/service_yaml_read.py b/src/docker_compose/compose/service_yaml_read.py new file mode 100644 index 0000000..bb73ed1 --- /dev/null +++ b/src/docker_compose/compose/service_yaml_read.py @@ -0,0 +1,60 @@ +from dataclasses import dataclass +from typing import Literal, NotRequired, TypedDict + +from docker_compose.compose.val_obj import Record +from docker_compose.Ts import T_Primitive +from docker_compose.yaml import YamlWrapper + +type T_NetAbc = str | Literal["proxy", "internal"] + + +class ServiceYamlReadData(TypedDict): + command: NotRequired[list[str]] + entrypoint: NotRequired[list[str]] + environment: NotRequired[dict[str, T_Primitive]] + image: str + labels: NotRequired[list[str]] + logging: NotRequired[dict[str, str]] + networks: NotRequired[list[str]] + security_opt: NotRequired[list[str]] + user: NotRequired[str] + volumes: NotRequired[list[str]] + + +@dataclass(frozen=True, slots=True) +class ServiceYamlRead(YamlWrapper[ServiceYamlReadData]): + @property + def sec_opts(self) -> frozenset[str]: + sec_opts = frozenset( + "no-new-privileges:true", + ) + sec = self.data.get("security_opt") + if not sec: + return sec_opts + return sec_opts.union(sec) + + @property + def labels(self) -> frozenset[str] | None: + org_name = Record.get_replace_name("org_name") + url = Record.get_replace_name("url") + traefik_labels = frozenset( + ( + f"traefik.http.routers.{org_name}.rule=Host(`{url}`)", + f"traefik.http.routers.{org_name}.entrypoints=websecure", + f"traefik.docker.network={org_name}_proxy", + f"traefik.http.routers.{org_name}.tls.certresolver=le", + ) + ) + labels = self.data.get("labels") + if not labels: + return + if "traefik.enable=true" not in labels: + return frozenset(labels) + return traefik_labels.union(labels) + + @property + def nets(self) -> frozenset[str] | None: + nets = self.data.get("networks") + if nets is None: + return + return frozenset(nets) diff --git a/src/docker_compose/compose/service_yaml_write.py b/src/docker_compose/compose/service_yaml_write.py new file mode 100644 index 0000000..e0c5dc4 --- /dev/null +++ b/src/docker_compose/compose/service_yaml_write.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import NotRequired + +from docker_compose.compose.service_yaml_read import ServiceYamlReadData +from docker_compose.yaml import YamlWrapper + + +class ServiceYamlWriteData(ServiceYamlReadData): + container_name: NotRequired[str] + restart: NotRequired[str] + + +@dataclass(frozen=True, slots=True) +class ServiceYamlWrite(YamlWrapper[ServiceYamlWriteData]): + pass diff --git a/src/docker_compose/compose/val_obj.py b/src/docker_compose/compose/val_obj.py new file mode 100644 index 0000000..7875628 --- /dev/null +++ b/src/docker_compose/compose/val_obj.py @@ -0,0 +1,81 @@ +from abc import ABCMeta, abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import final, override + + +@dataclass(frozen=True, slots=True) +class RecordVal(metaclass=ABCMeta): + @property + @abstractmethod + def str(self) -> str: + pass + + +@final +@dataclass(frozen=True, slots=True) +class Record[T: RecordVal]: + name: str + val: T + + def replace(self, string: str) -> str: + return string.replace(self.replace_name, self.val.str) + + @property + def replace_name(self) -> str: + return self.get_replace_name(self.name) + + @staticmethod + def get_replace_name(string: str) -> str: + return f"${{_{string.upper()}}}" + + +@final +@dataclass(frozen=True, slots=True) +class OrgVal(RecordVal): + val: str | None + + @property + @override + def str(self) -> str: + if self.val is None: + return "personal" + return self.val + + def is_valid(self) -> bool: + return self.val is not None + + +@final +@dataclass(frozen=True, slots=True) +class NameVal(RecordVal): + val: str + + @property + @override + def str(self) -> str: + return self.val + + +@final +@dataclass(frozen=True, slots=True) +class DataDir(RecordVal): + path: Path + + @property + @override + def str(self) -> str: + return str(self.path) + + +@final +@dataclass(frozen=True, slots=True) +class Url(RecordVal): + sub_url: str | None + + @property + @override + def str(self) -> str: + if self.sub_url is None: + return "" + return ".".join([self.sub_url, "ccamper7", "net"]) diff --git a/src/docker_compose/compose/volumes_yaml.py b/src/docker_compose/compose/volumes_yaml.py new file mode 100644 index 0000000..ed39dc8 --- /dev/null +++ b/src/docker_compose/compose/volumes_yaml.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import final + +from docker_compose.Ts import T_YamlDict +from docker_compose.yaml import YamlWrapper + +type VolYamlData = dict[str, T_YamlDict] + + +@final +@dataclass(frozen=True, slots=True) +class VolYaml(YamlWrapper[VolYamlData]): + pass + + +# def vols_from_path(path: Path) -> VolYamlData: +# return cast(VolYamlData, read_yml(path)) + + +# def vols_yaml_factory(self) -> Iterator[tuple[str, VolDataYaml]]: +# vols = self.volumes +# if vols is None: +# return +# for path in vols: +# yield path.stem, cast(VolDataYaml, read_yml(path)) diff --git a/src/docker_compose/util.py b/src/docker_compose/util.py index 71b0154..6637d48 100644 --- a/src/docker_compose/util.py +++ b/src/docker_compose/util.py @@ -1,7 +1,7 @@ from collections.abc import Mapping from typing import Any, cast -from compose.Ts import T_PrimDict, T_Primitive, T_PrimVal +from docker_compose.Ts import T_PrimDict, T_Primitive, T_PrimVal def merge_dicts[T: Mapping[Any, Any]](dict1: T, dict2: T) -> T: diff --git a/src/docker_compose/yaml.py b/src/docker_compose/yaml.py new file mode 100644 index 0000000..369fe90 --- /dev/null +++ b/src/docker_compose/yaml.py @@ -0,0 +1,42 @@ +import re +from collections.abc import ItemsView, Iterator, KeysView +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar, Protocol, cast, override + +import yaml + + +class ProtoMapping[K, V: object](Protocol): + def __getitem__(self, key: K, /) -> V: ... + def __iter__(self) -> Iterator[K]: ... + def __len__(self) -> int: ... + def __contains__(self, key: object, /) -> bool: ... + def keys(self) -> KeysView[K]: ... + def items(self) -> ItemsView[K, V]: ... + + +class TTypedyamldict(ProtoMapping[str, object], Protocol): + __required_keys__: ClassVar[frozenset[str]] + __optional_keys__: ClassVar[frozenset[str]] + + +class VerboseSafeDumper(yaml.SafeDumper): + @override + def ignore_aliases(self, data: object) -> bool: + return True + + +@dataclass(frozen=True, slots=True) +class YamlWrapper[T: ProtoMapping[str, object]]: + data: T + + @classmethod + def from_path(cls, path: Path): + with path.open("rt") as f: + return cls(cast(T, yaml.safe_load(f))) + + @property + def as_yaml(self) -> str: + _yaml = yaml.dump(self.data, Dumper=VerboseSafeDumper) + return re.sub(r"(^\s*-)", r" \g<1>", _yaml, flags=re.MULTILINE)