This commit is contained in:
2026-01-10 14:26:03 -06:00
parent 377e481803
commit 0ef6530f92
9 changed files with 156 additions and 243 deletions

View File

@@ -1,9 +1,11 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>Pswd</w>
<w>ccamper</w>
<w>certresolver</w>
<w>exts</w>
<w>pswd</w>
<w>stryten</w>
<w>traefik</w>
<w>websecure</w>

View File

@@ -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:

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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", ()))

View File

@@ -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

View File

@@ -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