sync
This commit is contained in:
@@ -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]
|
||||
@@ -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)),
|
||||
)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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)},
|
||||
# )
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
8
src/docker_compose/Ts.py
Normal file
8
src/docker_compose/Ts.py
Normal file
@@ -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]
|
||||
@@ -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]:
|
||||
|
||||
30
src/docker_compose/cfg/cfg_paths.py
Normal file
30
src/docker_compose/cfg/cfg_paths.py
Normal file
@@ -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),
|
||||
)
|
||||
27
src/docker_compose/cfg/cfg_paths_yaml.py
Normal file
27
src/docker_compose/cfg/cfg_paths_yaml.py
Normal file
@@ -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)
|
||||
15
src/docker_compose/cfg/org_data.py
Normal file
15
src/docker_compose/cfg/org_data.py
Normal file
@@ -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"))
|
||||
6
src/docker_compose/cfg/org_data_yaml.py
Normal file
6
src/docker_compose/cfg/org_data_yaml.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from typing import NotRequired, TypedDict
|
||||
|
||||
|
||||
class OrgDataYaml(TypedDict):
|
||||
org: str
|
||||
url: NotRequired[str]
|
||||
30
src/docker_compose/cfg/src_path.py
Normal file
30
src/docker_compose/cfg/src_path.py
Normal file
@@ -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)
|
||||
75
src/docker_compose/compose/compose.py
Normal file
75
src/docker_compose/compose/compose.py
Normal file
@@ -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
|
||||
20
src/docker_compose/compose/compose_yaml.py
Normal file
20
src/docker_compose/compose/compose_yaml.py
Normal file
@@ -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
|
||||
35
src/docker_compose/compose/dest_path.py
Normal file
35
src/docker_compose/compose/dest_path.py
Normal file
@@ -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)
|
||||
46
src/docker_compose/compose/net.py
Normal file
46
src/docker_compose/compose/net.py
Normal file
@@ -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)
|
||||
28
src/docker_compose/compose/net_args.py
Normal file
28
src/docker_compose/compose/net_args.py
Normal file
@@ -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)
|
||||
6
src/docker_compose/compose/net_args_yaml.py
Normal file
6
src/docker_compose/compose/net_args_yaml.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from typing import NotRequired, TypedDict
|
||||
|
||||
|
||||
class NetArgsYaml(TypedDict):
|
||||
name: str
|
||||
external: NotRequired[bool]
|
||||
8
src/docker_compose/compose/net_yaml.py
Normal file
8
src/docker_compose/compose/net_yaml.py
Normal file
@@ -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]
|
||||
39
src/docker_compose/compose/render.py
Normal file
39
src/docker_compose/compose/render.py
Normal file
@@ -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)
|
||||
73
src/docker_compose/compose/replace_args.py
Normal file
73
src/docker_compose/compose/replace_args.py
Normal file
@@ -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)
|
||||
80
src/docker_compose/compose/service.py
Normal file
80
src/docker_compose/compose/service.py
Normal file
@@ -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)
|
||||
60
src/docker_compose/compose/service_yaml_read.py
Normal file
60
src/docker_compose/compose/service_yaml_read.py
Normal file
@@ -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)
|
||||
15
src/docker_compose/compose/service_yaml_write.py
Normal file
15
src/docker_compose/compose/service_yaml_write.py
Normal file
@@ -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
|
||||
81
src/docker_compose/compose/val_obj.py
Normal file
81
src/docker_compose/compose/val_obj.py
Normal file
@@ -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"])
|
||||
25
src/docker_compose/compose/volumes_yaml.py
Normal file
25
src/docker_compose/compose/volumes_yaml.py
Normal file
@@ -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))
|
||||
@@ -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:
|
||||
|
||||
42
src/docker_compose/yaml.py
Normal file
42
src/docker_compose/yaml.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user