From 0ef6530f92d02303e67d71753f611dd93e1abef8695d86fffae3b97e380fd5ea Mon Sep 17 00:00:00 2001 From: Christian Camper Date: Sat, 10 Jan 2026 14:26:03 -0600 Subject: [PATCH] working --- .idea/dictionaries/project.xml | 2 + src/docker_compose/cfg/compose_paths.py | 32 +++---- src/docker_compose/cfg/dest_path.py | 8 +- src/docker_compose/cfg/env.py | 27 +++--- src/docker_compose/cfg/org.py | 106 ++++----------------- src/docker_compose/cfg/replace.py | 120 +++++++++++------------- src/docker_compose/compose/services.py | 8 +- src/docker_compose/util/Ts.py | 69 +++++++------- src/docker_compose/util/yaml_util.py | 27 +++--- 9 files changed, 156 insertions(+), 243 deletions(-) diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index ae1d885..239143a 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -1,9 +1,11 @@ + Pswd ccamper certresolver exts + pswd stryten traefik websecure diff --git a/src/docker_compose/cfg/compose_paths.py b/src/docker_compose/cfg/compose_paths.py index 832ad11..a9f644e 100644 --- a/src/docker_compose/cfg/compose_paths.py +++ b/src/docker_compose/cfg/compose_paths.py @@ -1,43 +1,42 @@ from collections.abc import Callable, Iterator, MutableMapping from dataclasses import dataclass, field from pathlib import Path -from typing import ClassVar, cast, final +from typing import cast, final import yaml -from docker_compose.cfg.org import OrgData -from docker_compose.cfg.replace import ReplaceDynamic, RecordCls +from docker_compose.cfg.org import App, Org +from docker_compose.cfg.replace import ReplaceDynamic, ReplaceStatic, ReplaceUnique from docker_compose.compose.services_yaml import ServiceYamlRead from docker_compose.compose.volume_yaml import VolYaml from docker_compose.util.Ts import T_YamlRW from docker_compose.util.yaml_util import path_to_typed, read_yaml -# _SERVICE = "service" @final @dataclass(frozen=True, slots=True) -class ServiceVal(RecordCls): - rep: ClassVar[str] = "service" +class ServiceVal(ReplaceStatic): + rep = ReplaceDynamic("service") @final @dataclass(frozen=True, slots=True) class ServicePath: path: Path - fqdn: ReplaceDynamic = field(init=False) - replace_pre: ReplaceDynamic = field(init=False) - replace_post: ReplaceDynamic = field(init=False) + fqdn: ReplaceUnique = field(init=False) + replace_pre: ReplaceStatic = field(init=False) + replace_post: ReplaceStatic = field(init=False) def __post_init__(self): setter = super(ServicePath, self).__setattr__ pre, post = ServiceVal.two_stage(self.path.stem) setter("replace_pre", pre) setter("replace_post", post) + setter( "fqdn", - ReplaceDynamic( - "fqdn", - f"{OrgData.org_app!s}_{pre!s}", + ReplaceUnique.auto_format( + "fqdn", str(Org.src.build_placeholder(App.src, pre.src)) ), ) @@ -51,7 +50,7 @@ class ServicePath: if not isinstance(data_dict, MutableMapping): raise TypeError path_to_typed(ServiceYamlRead, data_dict, self.path) - return data_dict # pyright: ignore[reportReturnType] + return cast(ServiceYamlRead, cast(object, data_dict)) @property def pre_render_funcs(self) -> Iterator[Callable[[str], str]]: @@ -87,13 +86,6 @@ class ComposePaths: for service in self.services: yield service.replace_post - # @classmethod - # def from_iters(cls, services: Iterable[ServicePath], volumes: Iterable[VolumePath]): - # return cls( - # frozenset(services), - # frozenset(volumes), - # ) - @property def volumes_k_v(self) -> Iterator[tuple[str, VolYaml]]: for path in self.volumes: diff --git a/src/docker_compose/cfg/dest_path.py b/src/docker_compose/cfg/dest_path.py index 622a632..d5ac40a 100644 --- a/src/docker_compose/cfg/dest_path.py +++ b/src/docker_compose/cfg/dest_path.py @@ -6,7 +6,7 @@ from typing import Self, final from docker_compose.cfg import DATA_ROOT from docker_compose.cfg.org import App, Org, OrgData -from docker_compose.cfg.replace import ReplaceDynamic +from docker_compose.cfg.replace import ReplaceUnique @final @@ -44,10 +44,10 @@ class ComposeFileRendered: @dataclass(frozen=True, slots=True) class DestPaths: # data_root = Record("data_root", str(DATA_ROOT)) - data_root = ReplaceDynamic.auto_format("data", str(DATA_ROOT)) - data_path = ReplaceDynamic( + data_root = ReplaceUnique.auto_format("data", str(DATA_ROOT)) + data_path = ReplaceUnique( data_root.src, - sep.join((data_root.src, Org.rep, App.rep)), + sep.join((data_root.src, str(Org), str(App))), ) data_dir: Path env_file: Path = field(init=False) diff --git a/src/docker_compose/cfg/env.py b/src/docker_compose/cfg/env.py index 27f2f92..c8e178a 100644 --- a/src/docker_compose/cfg/env.py +++ b/src/docker_compose/cfg/env.py @@ -6,16 +6,19 @@ from functools import partial from pathlib import Path from typing import Self, final -from docker_compose.cfg.replace import ReplaceDynamic +from docker_compose.cfg.replace import ReplaceDynamic, ReplaceStatic + + +@final +@dataclass(frozen=True, slots=True) +class Pswd(ReplaceStatic): + src = ReplaceDynamic("pswd") @final @dataclass class Env: - pswd = ReplaceDynamic.auto_format( - "pswd", - partial(secrets.token_urlsafe, 12), - ) + pswd = Pswd(partial(secrets.token_urlsafe, 12)) data: dict[str, str] @classmethod @@ -34,17 +37,17 @@ class Env: return cls({k: v for k, v in cls.get_lines(data)}) @property - def with_pass(self)->Iterator[tuple[str,str]]: - for k,v in self.data.items(): - if self.pswd.src not in v: - yield k,v + def with_pass(self) -> Iterator[tuple[str, str]]: + p = self.pswd + for k, v in self.data.items(): + if self.pswd.src.fmt not in v: + yield k, v continue - yield k,self.pswd(v) - + yield k, p(v) @property def as_txt(self) -> str: - return '\n'.join(sorted(map('='.join, self.with_pass))) + return "\n".join(sorted(map("=".join, self.with_pass))) @classmethod def copy(cls, src: Path, dest: Path) -> None: diff --git a/src/docker_compose/cfg/org.py b/src/docker_compose/cfg/org.py index 95b5540..904c10f 100644 --- a/src/docker_compose/cfg/org.py +++ b/src/docker_compose/cfg/org.py @@ -1,107 +1,40 @@ from collections.abc import Iterator from dataclasses import dataclass -from typing import Callable, ClassVar, Self, final, override +from typing import Callable, Self, final, override from docker_compose.cfg.org_yaml import OrgDataYaml -from docker_compose.cfg.replace import ReplaceDynamic, RecordCls - -# -# _ORG = "org" -# _APP = "name" -# Org = partial(Record, ORG) -# App = partial(Record, APP) +from docker_compose.cfg.replace import ReplaceDynamic, ReplaceStatic @final @dataclass(frozen=True, slots=True) -class Org(RecordCls): - rep: ClassVar[str] = "org" +class Org(ReplaceStatic): + src = ReplaceDynamic("org") @final @dataclass(frozen=True, slots=True) -class App(RecordCls): - rep: ClassVar[str] = "name" +class App(ReplaceStatic): + src = ReplaceDynamic("name") @final @dataclass(frozen=True, slots=True) -class Url(RecordCls): - rep: ClassVar[str] = "url" +class Url(ReplaceStatic): + src = ReplaceDynamic("url") @override - @classmethod - def from_str(cls, string: str | None) -> Self: - return super(Url, cls).from_str(".".join((string, "ccamper7", "net")) if string else "") - - # return Record("url", ".".join((val, "ccamper7", "net")) if val else "") - - -# -# @final -# @dataclass(frozen=True, slots=True) -# class Org: -# val: str -# replace: Record = field(init=False) -# -# def __post_init__(self) -> None: -# setter = super(Org, self).__setattr__ -# setter("replace", OrgVal(self.val)) - - -# @final -# @dataclass(frozen=True, slots=True) -# class AppVal(RecordCls[str]): -# old: ClassVar[RecordName] = RecordName("name") -# -# -# @final -# @dataclass(frozen=True, slots=True) -# class App: -# val: str -# replace: AppVal = field(init=False) -# -# def __post_init__(self) -> None: -# setter = super(App, self).__setattr__ -# setter("replace", AppVal(self.val)) - - -# @final -# @dataclass(frozen=True, slots=True) -# class UrlValNew: -# val: str | None -# -# @override -# def __str__(self) -> str: -# if not self.val: -# return "" -# return ".".join((self.val, "ccamper7", "net")) -# -# -# @final -# @dataclass(frozen=True, slots=True) -# class UrlVal(RecordCls[UrlValNew]): -# old = RecordName("url") -# -# -# @final -# @dataclass(frozen=True, slots=True) -# class Url: -# val: str | None -# replace: UrlVal = field(init=False) -# -# def __post_init__(self) -> None: -# setter = super(Url, self).__setattr__ -# setter("replace", UrlVal(UrlValNew(self.val))) + def __str__(self) -> str: + val = super(Url, self).__str__() + if not val: + return val + return ".".join((val, "ccamper7", "net")) @final @dataclass(frozen=True, slots=True) class OrgData: - org_app = ReplaceDynamic( - f"${Org.rep.upper()}_{Org.rep.upper()}", - f"{ReplaceDynamic.get_format(Org.rep)}_{ReplaceDynamic.get_format(App.rep)}", - ) + org_app = Org.src.build_placeholder(App.src) app: App org: Org url: Url @@ -109,9 +42,9 @@ class OrgData: @classmethod def from_dict(cls, app: str, org: str, data: OrgDataYaml) -> Self: return cls( - App.from_str(app), - Org.from_str(org), - Url.from_str(data.get("url")), + App(app), + Org(org), + Url(data.get("url")), ) @property @@ -123,8 +56,3 @@ class OrgData: @property def pre_render_funcs(self) -> Iterator[Callable[[str], str]]: yield self.org_app - - # def render_yaml(self, yaml: str) -> str: - # for func in self.render_funcs: - # yaml = func(yaml) - # return yaml diff --git a/src/docker_compose/cfg/replace.py b/src/docker_compose/cfg/replace.py index 3d834d3..5fe0a55 100644 --- a/src/docker_compose/cfg/replace.py +++ b/src/docker_compose/cfg/replace.py @@ -1,87 +1,75 @@ from collections.abc import Callable from dataclasses import dataclass, field -from typing import ClassVar, Self, override, final +from itertools import chain +from typing import ClassVar, Self, final, override -# -# class String(Protocol): -# @override -# def __str__(self) -> str: ... +def format_src(src: str) -> str: + return f"${{_{src.upper()}}}" -# def replace(self, string: str) -> str: -# return string.replace(str(self), str(self)) - - -# @dataclass(frozen=True, slots=True) -# class RecordCls[T_New: str | Callable[[None],str]]: -# old: ClassVar[str] -# new: T_New -# -# def __call__(self, string: str) -> str: -# return string.replace(str(self.old), self.new if isinstance(self.new, str) else self.new()) @final @dataclass(frozen=True, slots=True) -class ReplaceStatic: - val: 'str| ReplaceStatic' +class ReplaceUnique: + src: str + dest: str # | Callable[[], str] + + def __call__(self, string: str) -> str: + return string.replace(self.src, self.dest) + + @classmethod + def auto_format(cls, src: str, dest: str): + return cls(format_src(src), dest) + + # @override + # def __str__(self) -> str: + # if not self.dest: + # return "" + # if isinstance(self.dest, str): + # return self.dest + # return self.dest() + + +@final +@dataclass(frozen=True, slots=True) +class ReplaceDynamic: + val: str fmt: str = field(init=False) def __post_init__(self): - setter = super(ReplaceStatic, self).__setattr__ - setter('fmt', f"${{_{self.val.upper()}}}") + setter = super(ReplaceDynamic, self).__setattr__ + setter("fmt", format_src(self.val)) def __call__(self, string: str) -> str: - return string.replace( - self.fmt, - str(self) - ) - def __str__(self) -> str: - return self.val if isinstance(self.val, str) else self.val.fmt + return string.replace(self.fmt, self.val) -@dataclass(frozen=True, slots=True) -class ReplaceDynamic: - src: ReplaceStatic - dest: str | Callable[[], str] - - def __call__(self, string: str) -> str: - return string.replace( - self.src, - str(self), - ) - - @override - def __str__(self) -> str: - return self.dest if isinstance(self.dest, str) else self.dest() - # - # @staticmethod - # def get_format(val: str) -> str: - # return f"${{_{val.upper()}}}" - - @classmethod - def auto_format(cls, src: str, dest: str | Callable[[], str]) -> Self: - return cls(ReplaceStatic(src), dest) - - @classmethod - def from_str(cls, string: str) -> Self: - return cls.auto_format(string, string) - - - # @property - # def stage_two(self) -> 'Record': - # return Record.from_str(self.dest if isinstance(self.dest, str) else self.dest()) + # def __str__(self) -> str: + # return self.val if isinstance(self.val, str) else self.val.fmt + def build_placeholder(self, *args: "ReplaceDynamic"): + data = ((rep.val.upper(), rep.fmt) for rep in chain((self,), args)) + src: tuple[str, ...] + dest: tuple[str, ...] + src, dest = zip(*data) + return ReplaceUnique("_".join(src), "_".join(dest)) @dataclass(frozen=True, slots=True) -class RecordCls(ReplaceDynamic): - # val: ClassVar[str]# = 'org' - rep: ClassVar[ReplaceStatic] # = Record.get_format(val) +class ReplaceStatic: + src: ClassVar[ReplaceDynamic] + dest: None | str | Callable[[], str] + + def __call__(self, string: str) -> str: + return string.replace(self.src.fmt, str(self)) @override - @classmethod - def from_str(cls, string: str) -> Self: - return cls(cls.rep, string) + def __str__(self) -> str: + if not self.dest: + return "" + if isinstance(self.dest, str): + return self.dest + return self.dest() @classmethod - def two_stage(cls, dest: str) -> tuple[Self, Self]: - dest_var = ReplaceStatic(dest) - return cls(cls.rep, dest_var.fmt), cls(dest_var, dest) + def two_stage(cls, dest: str) -> tuple[Self, ReplaceDynamic]: + dest_var = ReplaceDynamic(dest) + return cls(dest_var.fmt), dest_var diff --git a/src/docker_compose/compose/services.py b/src/docker_compose/compose/services.py index a64c0fa..5a7489b 100644 --- a/src/docker_compose/compose/services.py +++ b/src/docker_compose/compose/services.py @@ -3,7 +3,7 @@ from typing import Self, final, override from docker_compose.cfg.compose_paths import ServicePath from docker_compose.cfg.org import OrgData, Url -from docker_compose.cfg.replace import ReplaceDynamic +from docker_compose.cfg.replace import ReplaceStatic, ReplaceUnique from docker_compose.compose.services_yaml import ( HealthCheck, ServiceYamlRead, @@ -17,7 +17,7 @@ from docker_compose.util.Ts import T_Primitive class Service: _traefik_labels = frozenset( ( - f"traefik.http.routers.{OrgData.org_app.src}.rule=Host(`{Url.rep}`)", + f"traefik.http.routers.{OrgData.org_app!s}.rule=Host(`{Url!s}`)", f"traefik.http.routers.{OrgData.org_app.src}.entrypoints=websecure", f"traefik.docker.network={OrgData.org_app.src}_proxy", f"traefik.http.routers.{OrgData.org_app.src}.tls.certresolver=le", @@ -35,7 +35,7 @@ class Service: _sec_opts = frozenset(("no-new-privileges:true",)) # service_name: str # service_val: ServiceVal - fqdn: ReplaceDynamic + fqdn: ReplaceUnique command: tuple[str, ...] entrypoint: tuple[str, ...] environment: dict[str, T_Primitive] @@ -57,7 +57,7 @@ class Service: return cls.from_dict(path.fqdn, path.as_dict) @classmethod - def from_dict(cls, fqdn: ReplaceDynamic, data: ServiceYamlRead) -> Self: + def from_dict(cls, fqdn: ReplaceStatic, data: ServiceYamlRead) -> Self: # helper = ServiceYamlProps(data) labels = frozenset(data.get("labels", ())) # ports = (f'"{p}"' for p in data.get("ports", ())) diff --git a/src/docker_compose/util/Ts.py b/src/docker_compose/util/Ts.py index dfede4e..bc9744b 100644 --- a/src/docker_compose/util/Ts.py +++ b/src/docker_compose/util/Ts.py @@ -2,26 +2,31 @@ from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, Set from types import GenericAlias, UnionType from typing import ( ClassVar, - Never, Protocol, TypeAliasType, cast, get_args, get_origin, - overload, ) +type T_Primitive = None | bool | int | str -class TypedYamlDict[K: object, V: object](Protocol): - def __getitem__(self, key: str | K, /) -> V: ... +# type T_TDict = MutableMapping[T_Primitive, T_Prim] +type T_TIters = list[T_Prim] +type T_T = T_Primitive | T_TIters | TypedYamlDict + + +class TypedYamlDict(Protocol): + def __getitem__(self, key: str, /) -> T_T: ... # def __setitem__(self, key: str, value: V, /) -> V: ... - def __delitem__(self, key: Never | K, /) -> None: ... - def __contains__(self, key: K, /) -> bool: ... - def __iter__(self) -> Iterator[K]: ... + # def __delitem__(self, key: Never | K, /) -> None: ... + def __contains__(self, key: str, /) -> bool: ... + def __iter__(self) -> Iterator[str]: ... def __len__(self) -> int: ... - def keys(self) -> KeysView[K]: ... - def items(self) -> ItemsView[K, V]: ... - def pop(self, key: Never | K, /) -> V: ... + def keys(self) -> KeysView[str]: ... + def items(self) -> ItemsView[str, T_T]: ... + + # def pop(self, key: Never | K, /) -> V: ... # def popitem(self) -> tuple[K, V]: ... @@ -45,7 +50,6 @@ class TypedYamlDict[K: object, V: object](Protocol): # # is_typed_dict_test(x) -type T_Primitive = None | bool | int | str type T_PrimIters = tuple[T_Prim, ...] | list[T_Prim] | Set[T_Prim] | Iterator[T_Prim] type T_PrimDict = MutableMapping[T_Primitive, T_Prim] @@ -57,45 +61,40 @@ type T_YamlRW = T_YamlIters | T_YamlDict type T_Yaml = T_Primitive | T_YamlRW -type T_YamlPostDict = TypedYamlDict[str, T_YamlPost] +type T_YamlPostDict = MutableMapping[str, T_YamlPost] type T_YamlPostRes = tuple[T_YamlPost, ...] | T_YamlPostDict type T_YamlPost = T_Primitive | T_YamlPostRes def get_union_types(annotations: UnionType) -> Iterator[type]: for annotation in get_args(annotations): # pyright: ignore[reportAny] + annotation = cast(TypeAliasType | GenericAlias | UnionType | type, annotation) if isinstance(annotation, TypeAliasType): - annotation = annotation.__value__ # pyright: ignore[reportAny] + yield from get_types( + cast(GenericAlias | UnionType | type, annotation.__value__) + ) + continue if isinstance(annotation, UnionType): yield from get_union_types(annotation) continue - yield get_types(annotation) # pyright: ignore[reportAny] - - -@overload -def get_types(annotation: UnionType) -> tuple[type]: - pass - - -@overload -def get_types(annotation: GenericAlias) -> type: - pass - - -@overload -def get_types(annotation: TypeAliasType) -> type | tuple[type, ...]: - pass + yield from get_types(annotation) def get_types( - annotation: TypeAliasType | GenericAlias | UnionType, -) -> type | tuple[type, ...]: + annotation: TypeAliasType | GenericAlias | UnionType | type, +) -> Iterator[type]: if isinstance(annotation, TypeAliasType): - annotation = annotation.__value__ # pyright: ignore[reportAny] + yield from get_types( + cast(GenericAlias | UnionType | type, annotation.__value__) + ) + return if isinstance(annotation, GenericAlias): # print(annotation) # print(get_origin(annotation)) - return get_origin(annotation) + yield get_origin(annotation) + return if isinstance(annotation, UnionType): - return tuple(get_union_types(annotation)) - return cast(type, annotation) # pyright: ignore[reportInvalidCast] + yield from get_union_types(annotation) + return + yield annotation + return diff --git a/src/docker_compose/util/yaml_util.py b/src/docker_compose/util/yaml_util.py index 2648106..b9b7937 100644 --- a/src/docker_compose/util/yaml_util.py +++ b/src/docker_compose/util/yaml_util.py @@ -1,7 +1,12 @@ import re from collections.abc import Iterator, MutableMapping, Set from pathlib import Path -from typing import cast, get_type_hints, is_typeddict, override +from typing import ( + cast, + get_type_hints, + is_typeddict, + override, +) import yaml @@ -101,7 +106,7 @@ def read_yaml(path: Path) -> T_YamlPostRes: return yaml.safe_load(f) # pyright: ignore[reportAny] -def read_typed_yaml[T: TypedYamlDict[object, object]]( +def read_typed_yaml[T: TypedYamlDict]( type_: type[T], path: Path, ) -> T: @@ -112,7 +117,7 @@ def read_typed_yaml[T: TypedYamlDict[object, object]]( def path_to_typed( - type_: type[TypedYamlDict[object, object]], + type_: type[TypedYamlDict], data: T_YamlDict, path: Path, ) -> None: @@ -124,7 +129,7 @@ def path_to_typed( def validate_typed_dict( - t: type[TypedYamlDict[object, object]], + t: type[TypedYamlDict], data: T_YamlDict, ) -> None: keys = frozenset(data.keys()) @@ -136,21 +141,17 @@ def validate_typed_dict( raise KeyError(f"extra key(s): {', '.join(map(str, extra))}") hints = get_type_hints(t) for key, val in data.items(): - t2 = hints[key] # pyright: ignore[reportAny] - if is_typeddict(t2): # pyright: ignore[reportAny] - validate_typed_dict(t2, cast(T_YamlDict, val)) # pyright: ignore[reportAny] + t2 = cast(type, cast(object, hints[key])) + if is_typeddict(t2): + validate_typed_dict(t2, cast(T_YamlDict, val)) continue # try: # print(t2) # print(get_types(t2)) - t2 = get_types(t2) + t2 = tuple(get_types(t2)) if not isinstance(val, t2): - msg = ( - ", ".join(t.__name__ for t in t2) - if isinstance(t2, tuple) - else t.__name__ - ) + msg = ", ".join(t.__name__ for t in t2) e = TypeError(f"key: {key} expected *{msg}*, got *{type(val).__name__}*") e.add_note(f"key: {key!s}") raise e