Compare commits

...

2 Commits

Author SHA256 Message Date
aba05be048 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.idea/.gitignore
#	.idea/dictionaries/project.xml
#	.idea/inspectionProfiles/profiles_settings.xml
#	.idea/misc.xml
#	.idea/modules.xml
#	src/docker_compose/__init__.py
#	src/docker_compose/cfg/__init__.py
#	src/docker_compose/cfg/cfg_paths.py
#	src/docker_compose/cfg/src_path.py
#	src/docker_compose/compose/compose.py
#	src/docker_compose/compose/compose_yaml.py
#	src/docker_compose/compose/net.py
#	src/docker_compose/compose/net_yaml.py
2026-01-08 23:05:56 -06:00
38b6807e70 working 2026-01-08 23:04:58 -06:00
36 changed files with 1190 additions and 167 deletions

0
.gitignore vendored Normal file → Executable file
View File

5
.idea/.gitignore generated vendored
View File

@@ -1,5 +1,10 @@
# Default ignored files # Default ignored files
/shelf/ /shelf/
/workspace.xml /workspace.xml
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests # Editor-based HTTP Client requests
/httpRequests/ /httpRequests/

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

11
.idea/compose_gen.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="uv (compose_gen)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,7 +1,12 @@
<component name="ProjectDictionaryState"> <component name="ProjectDictionaryState">
<dictionary name="project"> <dictionary name="project">
<words> <words>
<w>ccamper</w>
<w>certresolver</w>
<w>exts</w>
<w>stryten</w>
<w>traefik</w> <w>traefik</w>
<w>websecure</w>
</words> </words>
</dictionary> </dictionary>
</component> </component>

View File

@@ -1,5 +1,6 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<settings> <settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" /> <option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" /> <version value="1.0" />
</settings> </settings>

13
.idea/misc.xml generated
View File

@@ -1,7 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.13 (compose_gen_uv)" /> <option name="sdkName" value="uv (compose_gen)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="uv (compose_gen)" project-jdk-type="Python SDK" />
<component name="PyrightConfiguration">
<option name="enabled" value="true" />
</component>
<component name="RuffConfiguration">
<option name="enabled" value="true" />
</component>
<component name="TyConfiguration">
<option name="enabled" value="true" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (compose_gen_uv)" project-jdk-type="Python SDK" />
</project> </project>

2
.idea/modules.xml generated
View File

@@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/compose_gen_uv.iml" filepath="$PROJECT_DIR$/.idea/compose_gen_uv.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/compose_gen.iml" filepath="$PROJECT_DIR$/.idea/compose_gen.iml" />
</modules> </modules>
</component> </component>
</project> </project>

0
.python-version Normal file → Executable file
View File

0
.vscode/settings.json vendored Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

0
docs/workflow.md Normal file → Executable file
View File

12
pyproject.toml Normal file → Executable file
View File

@@ -1,19 +1,23 @@
[project] [project]
name = "compose" name = "docker_compose"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
authors = [{ name = "Christian Camper", email = "ccamper7@gmail.com" }] authors = [{ name = "Christian Camper", email = "ccamper7@gmail.com" }]
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"basedpyright>=1.36.1", "basedpyright>=1.37.0",
"pyyaml>=6.0.3", "pyyaml>=6.0.3",
"ruff>=0.14.9", "ruff>=0.14.10",
"ty>=0.0.10",
] ]
[project.scripts] [project.scripts]
compose = "compose:main" docker_compose = "docker_compose:main"
[build-system] [build-system]
requires = ["uv_build>=0.9.17,<0.10.0"] requires = ["uv_build>=0.9.17,<0.10.0"]
build-backend = "uv_build" build-backend = "uv_build"
[tool.pyright]
#reportExplicitAny = false

View File

@@ -1,21 +1,36 @@
from collections.abc import Iterable, Iterator from collections.abc import Iterator
from docker_compose.cfg import CFG_ROOT, TRAEFIK_PATH from docker_compose.cfg import CFG_ROOT, TRAEFIK_PATH
from docker_compose.compose.render import Rendered from docker_compose.compose.net_yaml import NetArgsYaml
from docker_compose.compose.rendered import Rendered
from docker_compose.util.yaml_util import to_yaml
def load_all() -> Iterable[Rendered]: def load_all() -> Iterator[Rendered]:
for _dir in CFG_ROOT.iterdir(): for path in CFG_ROOT.iterdir():
yield Rendered.from_path(_dir) if path == TRAEFIK_PATH:
continue
yield from Rendered.from_path(path)
def render_all() -> Iterator[str]: def render_all() -> Iterator[str]:
for rendered in load_all(): for rendered in load_all():
rendered.write_all() rendered.write()
rendered.write_bind_vols()
rendered.mk_bind_vols()
yield from rendered.proxy_nets yield from rendered.proxy_nets
if __name__ == "__main__": if __name__ == "__main__":
# renders = render_all() # renders = render_all()
nets = frozenset(render_all()) nets = frozenset(render_all())
traefik = Rendered.from_path(TRAEFIK_PATH) traefik = next(Rendered.from_path(TRAEFIK_PATH))
data = traefik.as_dict
data["networks"] = {net: NetArgsYaml(name=f"{net}_proxy") for net in nets}
cfg = traefik.cfg
data["services"]["traefik"]["networks"] = nets
template = cfg.pre_render(to_yaml(data))
cfg.src_paths.compose_file.write(template)
cfg.dest_paths.compose_file.write(cfg.render(template))
traefik.write_bind_vols()
traefik.mk_bind_vols()

View File

@@ -1,5 +1,7 @@
from pathlib import Path from pathlib import Path
CFG_ROOT = Path("/data/cfg") ROOT = Path("/nas")
DATA_ROOT = Path("/data") DATA_ROOT = ROOT.joinpath("apps")
TRAEFIK_PATH = Path("/data/traefik") CFG_ROOT = ROOT.joinpath("cfg/templates")
TRAEFIK_PATH = CFG_ROOT.joinpath("traefik")
# TEMPLATE_DIR = CFG_ROOT.joinpath("templates")

View File

@@ -1,9 +1,12 @@
from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from itertools import chain
from shutil import copyfile
from typing import Self, final from typing import Self, final
from docker_compose.cfg.cfg_paths_yaml import CfgYaml from docker_compose.cfg.compose_paths import ComposePaths
from docker_compose.cfg.org_data import OrgData from docker_compose.cfg.dest_path import DestPaths
from docker_compose.cfg.org import OrgData
from docker_compose.cfg.src_path import SrcPaths from docker_compose.cfg.src_path import SrcPaths
@@ -11,20 +14,49 @@ from docker_compose.cfg.src_path import SrcPaths
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class CfgData: class CfgData:
src_paths: SrcPaths src_paths: SrcPaths
name: str org_data: OrgData
services: frozenset[Path] compose_paths: ComposePaths
volumes: frozenset[Path] | None dest_paths: DestPaths
org_data: frozenset[OrgData]
@classmethod @classmethod
def from_src_paths(cls, src_paths: SrcPaths) -> Self: def from_src_paths(cls, src_paths: SrcPaths) -> Iterator[Self]:
cfg_yaml = CfgYaml.from_src_paths(src_paths) for org, args in src_paths.org_file.as_dict.items():
cfg_root = src_paths.cfg_dir.joinpath org = OrgData.from_dict(src_paths.app, org, args)
vols = cfg_yaml.data.get("volumes") dest = DestPaths.from_org(org)
return cls( yield cls(
src_paths, src_paths,
src_paths.cfg_dir.name, org,
frozenset(cfg_root(path) for path in cfg_yaml.data["services"]), ComposePaths.from_iters(
frozenset(cfg_root(path) for path in vols) if vols else None, src_paths.service_dir.files,
frozenset(cfg_yaml.orgs_data), src_paths.vol_dir.files,
),
dest,
) )
def pre_render(self, data: str) -> str:
for func in chain(
self.org_data.pre_render_funcs, self.dest_paths.pre_render_funcs
):
data = func(data)
return data
def render(self, data: str) -> str:
for func in chain(
self.org_data.render_funcs,
self.dest_paths.render_funcs,
self.compose_paths.render_funcs,
):
data = func(data)
return data
def render_all(self, data: str) -> str:
for func in (self.pre_render, self.render):
# noinspection PyArgumentList
data = func(data)
return data
def mk_compose_env(self) -> None:
src = self.src_paths.env_file
dest = self.dest_paths.env_file
if src.exists() and not dest.exists():
_ = copyfile(src, dest)

View File

@@ -0,0 +1,112 @@
from collections.abc import Callable, Iterable, Iterator, MutableMapping
from dataclasses import dataclass, field
from pathlib import Path
from typing import Self, cast, final, override
import yaml
from docker_compose.cfg.org import OrgData
from docker_compose.cfg.record import Record, RecordCls, RecordName
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
@final
@dataclass(frozen=True, slots=True)
class ServiceValNew:
val: Path
@override
def __str__(self) -> str:
return str(RecordName(self.val.stem))
@property
def replace(self) -> Record[Self, str]:
return Record(self, self.val.stem)
@final
@dataclass(frozen=True, slots=True)
class ServiceVal(RecordCls[ServiceValNew]):
old = RecordName("service")
@property
def stage_two(self):
return self.new.replace
@final
@dataclass(frozen=True, slots=True)
class ServicePath:
fqdn = Record[RecordName, str](
RecordName("fqdn"),
f"{OrgData.org_app.old!s}_{ServiceVal.old!s}",
)
path: Path
replace: ServiceVal = field(init=False)
def __post_init__(self):
setter = super(ServicePath, self).__setattr__
setter("replace", ServiceVal(ServiceValNew(self.path)))
@property
def as_dict(self) -> ServiceYamlRead:
with self.path.open("rt") as f:
data_str = f.read()
for func in self.pre_render_funcs:
data_str = func(data_str)
data_dict: T_YamlRW = yaml.safe_load(data_str) # pyright: ignore[reportAny]
if not isinstance(data_dict, MutableMapping):
raise TypeError
path_to_typed(ServiceYamlRead, data_dict, self.path)
return data_dict # pyright: ignore[reportReturnType]
@property
def pre_render_funcs(self) -> Iterator[Callable[[str], str]]:
yield self.fqdn
yield self.replace
@property
def render_funcs(self) -> Iterator[Callable[[str], str]]:
yield self.replace.stage_two
@final
@dataclass(frozen=True, slots=True)
class VolumePath:
path: Path
@property
def as_k_v(self) -> tuple[str, VolYaml]:
return self.path.stem, self.as_dict
@property
def as_dict(self) -> VolYaml:
return cast(VolYaml, cast(object, read_yaml(self.path)))
@final
@dataclass(frozen=True, slots=True)
class ComposePaths:
services: frozenset[ServicePath]
volumes: frozenset[VolumePath]
@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:
yield path.as_k_v
@property
def render_funcs(self) -> Iterator[Callable[[str], str]]:
for path in self.services:
yield from path.render_funcs

View File

@@ -0,0 +1,81 @@
from collections.abc import Callable, Iterator
from dataclasses import dataclass, field
from os import sep
from pathlib import Path
from typing import Self, final
from docker_compose.cfg import DATA_ROOT
from docker_compose.cfg.org import AppVal, OrgData, OrgVal
from docker_compose.cfg.record import Record, RecordName
@final
@dataclass(frozen=True, slots=True)
class ComposeFileRendered:
path: Path
def write(self, data: str) -> None:
print(self.path)
self.path.parent.mkdir(parents=True, exist_ok=True)
with self.path.open("wt") as f:
_ = f.write(data)
# @final
# @dataclass(frozen=True, slots=True)
# class DataDirReplace(RecordCls[Path]):
# old = RecordName("data")
#
#
# @final
# @dataclass(frozen=True, slots=True)
# class DataDir:
# val: Path
# replace: DataDirReplace = field(init=False)
#
# def __post_init__(self) -> None:
# setter = super().__setattr__
# setter("replace", DataDirReplace(self.val))
#
# @classmethod
# def from_org(cls, org: OrgData) -> Self:
# cls(DATA_ROOT.joinpath(org.org.val, org.app.val))
@final
@dataclass(frozen=True, slots=True)
class DestPaths:
data_root = Record[RecordName, Path](RecordName("data_root"), DATA_ROOT)
data_path = Record[RecordName, str](
RecordName("data"),
sep.join((str(data_root.old), str(OrgVal.old), str(AppVal.old))),
)
data_dir: Path
env_file: Path = field(init=False)
compose_file: ComposeFileRendered = field(init=False)
def __post_init__(self) -> None:
setter = super(DestPaths, self).__setattr__
path_join = self.data_dir.joinpath
setter("env_file", path_join(".env"))
setter("compose_file", ComposeFileRendered(path_join("docker-compose.yml")))
@classmethod
def from_org(cls, org: OrgData) -> Self:
return cls.from_path(DATA_ROOT.joinpath(org.org.val, org.app.val))
@classmethod
def from_path(cls, path: Path) -> Self:
return cls(path)
# def mk_compose_dir(self) -> None:
# folder = self.data_dir
# if folder.exists():
# return
# folder.mkdir(parents=True)
@property
def pre_render_funcs(self) -> Iterator[Callable[[str], str]]:
yield self.data_path
@property
def render_funcs(self) -> Iterator[Callable[[str], str]]:
yield self.data_root

View File

@@ -0,0 +1,104 @@
from collections.abc import Iterator
from dataclasses import dataclass, field
from typing import Callable, ClassVar, Self, final, override
from docker_compose.cfg.org_yaml import OrgDataYaml
from docker_compose.cfg.record import Record, RecordCls, RecordName
@final
@dataclass(frozen=True, slots=True)
class OrgVal(RecordCls[str]):
old: ClassVar[RecordName] = RecordName("org")
@final
@dataclass(frozen=True, slots=True)
class Org:
val: str
replace: OrgVal = 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)))
@final
@dataclass(frozen=True, slots=True)
class OrgData:
org_app = Record[RecordName, str](
RecordName(f"{OrgVal.old.val}_{AppVal.old.val}"),
f"{OrgVal.old!s}_{AppVal.old!s}",
)
app: App
org: Org
url: Url
@classmethod
def from_dict(cls, app: str, org: str, data: OrgDataYaml) -> Self:
return cls(
App(app),
Org(org),
Url(data.get("url")),
)
@property
def render_funcs(self) -> Iterator[Callable[[str], str]]:
yield self.app.replace
yield self.org.replace
yield self.url.replace
@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

@@ -0,0 +1,9 @@
from typing import Literal, NotRequired, TypedDict
class OrgDataYaml(TypedDict):
# org: str
url: NotRequired[str]
type OrgYaml = dict[Literal["ccamper7", "c4", "stryten"], OrgDataYaml]

View File

@@ -0,0 +1,38 @@
from dataclasses import dataclass
from typing import ClassVar, Protocol, final, override
class String(Protocol):
@override
def __str__(self) -> str: ...
@final
@dataclass(frozen=True, slots=True)
class RecordName:
val: str
@override
def __str__(self) -> str:
return f"${{_{str(self.val).upper()}}}"
# def replace(self, string: str) -> str:
# return string.replace(str(self), str(self))
@dataclass(frozen=True, slots=True)
class RecordCls[T_New: String]:
old: ClassVar[String]
new: T_New
def __call__(self, string: str) -> str:
return string.replace(str(self.old), str(self.new))
@dataclass(frozen=True, slots=True)
class Record[T_Old: String, T_New: String]:
old: T_Old
new: T_New
def __call__(self, string: str) -> str:
return string.replace(str(self.old), str(self.new))

View File

@@ -1,30 +1,101 @@
from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Self, final from typing import Self, cast, final
from docker_compose.cfg.compose_paths import ServicePath, VolumePath
from docker_compose.cfg.org_yaml import OrgYaml
from docker_compose.util.Ts import T_YamlDict, T_YamlRW
from docker_compose.util.yaml_util import read_yaml, write_yaml
YAML_EXTS = frozenset((".yml", ".yaml"))
class ComposeFileTemplate(Path):
def write_dict(self, data: T_YamlDict) -> None:
write_yaml(data, self)
def write(self, data: str) -> None:
with self.open("wt") as f:
_ = f.write(data)
class OrgFile(Path):
@property
def as_dict(self) -> OrgYaml:
return cast(OrgYaml, cast(object, read_yaml(self)))
class YamlDir(Path):
@property
def yaml_files(self) -> Iterator[Path]:
if not self:
raise FileNotFoundError(self)
for service in self.iterdir():
if service.suffix not in YAML_EXTS:
continue
yield service
def __bool__(self) -> bool:
return self.exists()
class CfgDir(YamlDir):
@property
def cfg_file(self) -> OrgFile:
for file in self.yaml_files:
if file.stem != "cfg":
continue
return OrgFile(file)
raise FileNotFoundError(self.joinpath("cfg.y(a)ml"))
class ServiceDir(YamlDir):
@property
def files(self) -> Iterator[ServicePath]:
for file in self.yaml_files:
yield ServicePath(file)
class VolumesDir(YamlDir):
@property
def files(self) -> Iterator[VolumePath]:
try:
for file in self.yaml_files:
yield VolumePath(file)
except FileNotFoundError:
return
class VolumeData(Path):
def write(self, data: T_YamlRW) -> None:
write_yaml(data, self)
@final @final
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class SrcPaths: class SrcPaths:
cfg_dir: Path cfg_dir: CfgDir
cfg_file: Path org_file: OrgFile
env_file: Path env_file: Path
service_dir: ServiceDir
vol_dir: VolumesDir
compose_file: ComposeFileTemplate
volume_data: VolumeData
@classmethod @classmethod
def from_path(cls, src: Path) -> Self: def from_path(cls, src: Path) -> Self:
cfg_dir = CfgDir(src)
return cls( return cls(
cfg_dir=src, cfg_dir,
cfg_file=src.joinpath("cfg.yml"), cfg_dir.cfg_file,
env_file=src.joinpath(".env"), src.joinpath(".env"),
ServiceDir(src.joinpath("services")),
VolumesDir(src.joinpath("volumes")),
ComposeFileTemplate(src.joinpath("docker-compose.yml")),
VolumeData(src.joinpath("volume_paths.yml")),
) )
# def src_path_get_services(src_paths: SrcPaths, data: CfgDataYaml): @property
# for path in data["services"]: def app(self) -> str:
# yield src_paths.cfg_dir.joinpath(path) return self.cfg_dir.stem
# 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)

View File

View File

@@ -1,75 +1,69 @@
from abc import ABCMeta
from collections.abc import Iterator from collections.abc import Iterator
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Self from typing import Self
from docker_compose.cfg.cfg_paths import CfgData from docker_compose.cfg.cfg_paths import CfgData
from docker_compose.cfg.org import OrgData
from docker_compose.cfg.src_path import SrcPaths from docker_compose.cfg.src_path import SrcPaths
from docker_compose.compose.compose_yaml import ComposeYaml, ComposeYamlData from docker_compose.compose.compose_yaml import ComposeYaml
from docker_compose.compose.net import Net from docker_compose.compose.net import Net
from docker_compose.compose.replace_args import ReplaceArgs from docker_compose.compose.services import Service
from docker_compose.compose.service import Service from docker_compose.compose.volume_yaml import VolYaml
from docker_compose.compose.volumes_yaml import VolYaml from docker_compose.util.yaml_util import to_yaml
@dataclass(frozen=True, slots=True) @dataclass(slots=True)
class Compose(metaclass=ABCMeta): class Compose:
cfg: CfgData cfg: CfgData
services: dict[str, Service] services: frozenset[Service]
networks: Net | None networks: Net
volumes: dict[str, VolYaml] | None volumes: dict[str, VolYaml]
replace_args: frozenset[ReplaceArgs] # replace_args: ReplaceArgs
@classmethod @classmethod
def from_path(cls, path: Path): def from_path(cls, path: Path) -> Iterator[Self]:
return cls.from_src_path(SrcPaths.from_path(path)) return cls.from_src_path(SrcPaths.from_path(path))
@classmethod @classmethod
def from_src_path(cls, src_paths: SrcPaths) -> Self: def from_src_path(cls, src_paths: SrcPaths) -> Iterator[Self]:
return cls.from_cfg(CfgData.from_src_paths(src_paths)) for cfg in CfgData.from_src_paths(src_paths):
yield cls.from_cfg(cfg)
@classmethod @classmethod
def from_cfg(cls, cfg_data: CfgData) -> Self: def from_cfg(cls, cfg_data: CfgData) -> Self:
services = {path.stem: Service.from_path(path) for path in cfg_data.services} # services = {
vols = {path.stem: VolYaml.from_path(path) for path in cfg_data.services} # path.stem: Service.from_dict(path, data)
nets = Net.from_service_list(services.values()) # for path, data in cfg_data.compose_paths.services_k_v
# }
services = frozenset(
Service.from_path(path) for path in cfg_data.compose_paths.services
)
return cls( return cls(
cfg_data, cfg_data,
services, services,
nets if nets else None, Net.from_service_list(services),
vols if vols else None, dict(cfg_data.compose_paths.volumes_k_v),
frozenset(
ReplaceArgs.from_cfg_data(cfg_data, org) for org in cfg_data.org_data
),
) )
# @property
# def app(self) -> str:
# return self.cfg.src_paths.app
@property @property
def as_dict(self) -> ComposeYaml: def as_dict(self) -> ComposeYaml:
data = ComposeYamlData( return ComposeYaml(
name=self.cfg.name, name=str(OrgData.org_app.old),
services={k: v.as_dict.data for k, v in self.services.items()}, services={
service.service_name: service.as_dict for service in self.services
},
networks=self.networks.as_dict,
volumes=self.volumes,
) )
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 @property
def as_yaml(self) -> str: def as_template(self) -> str:
return self.as_dict.as_yaml return self.cfg.pre_render(to_yaml(self.as_dict))
@property def write_template(self):
def proxys(self) -> Iterator[str]: self.cfg.src_paths.compose_file.write(self.as_template)
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

View File

@@ -1,20 +1,12 @@
from dataclasses import dataclass from typing import TypedDict
from typing import NotRequired, TypedDict, final
from docker_compose.compose.net_yaml import NetYaml from docker_compose.compose.net_yaml import NetYaml
from docker_compose.compose.service_yaml_write import ServiceYamlWriteData from docker_compose.compose.services_yaml import ServiceYamlWrite
from docker_compose.compose.volumes_yaml import VolYamlData from docker_compose.compose.volume_yaml import VolYaml
from docker_compose.yaml import YamlWrapper
class ComposeYamlData(TypedDict): class ComposeYaml(TypedDict):
name: str name: str
services: dict[str, ServiceYamlWriteData] services: dict[str, ServiceYamlWrite]
networks: NotRequired[NetYaml] networks: NetYaml | None
volumes: NotRequired[dict[str, VolYamlData]] volumes: dict[str, VolYaml] | None
@final
@dataclass(frozen=True, slots=True)
class ComposeYaml(YamlWrapper[ComposeYamlData]):
pass

View File

@@ -1,46 +1,61 @@
from collections.abc import Iterable from collections.abc import Iterable, Iterator
from dataclasses import dataclass from dataclasses import dataclass
from typing import final from typing import Self, final, override
from docker_compose.compose.net_args import NetArgs from docker_compose.cfg.org import OrgData
from docker_compose.compose.net_yaml import NetYaml from docker_compose.compose.net_yaml import NetArgsYaml, NetYaml
from docker_compose.compose.service import Service from docker_compose.compose.services import Service
# @final
# @dataclass(frozen=True, slots=True)
# class Net:
# internal: NetArgs | None
# proxy: NetArgs | None
# @property @final
# def as_dict(self) -> NetYaml: @dataclass(frozen=True, slots=True)
# yaml_dict = NetYaml() class NetArgs:
# if self.internal is not None: name: str
# yaml_dict["internal"] = self.internal.as_dict
# if self.proxy is not None: @property
# yaml_dict["proxy"] = self.proxy.as_dict def as_dict(self) -> NetArgsYaml:
# return yaml_dict yaml_dict = NetArgsYaml(
name=str(self),
)
if self.external:
yaml_dict["external"] = self.external
return yaml_dict
# @property
# def as_key_dict(self) -> tuple[str, NetArgsYaml]:
# return str(self), self.as_dict
@override
def __str__(self) -> str:
return f"{OrgData.org_app.old!s}_{self.name}"
@property
def external(self) -> bool:
return self.name == "proxy"
@final @final
@dataclass @dataclass
class Net: class Net:
data: dict[str, NetArgs] data: frozenset[NetArgs]
@classmethod @classmethod
def from_service_list(cls, args: Iterable[Service]): def from_service_list(cls, args: Iterable[Service]) -> Self:
return cls.from_list( return cls.from_list(
{net for service in args if service.networks for net in service.networks} frozenset(net for service in args for net in service.networks)
) )
@classmethod @classmethod
def from_list(cls, args: Iterable[str]): def from_list(cls, args: frozenset[str]) -> Self:
return cls({net: NetArgs(net, NetArgs.is_proxy_check(net)) for net in args}) return cls(frozenset(NetArgs(arg) for arg in args))
@property @property
def as_dict(self) -> NetYaml: def as_dict(self) -> NetYaml:
return {name: net_args.as_dict for name, net_args in self.data.items()} return {net.name: net.as_dict for net in self.data}
def __bool__(self) -> bool: @property
return bool(self.data) def proxys(self) -> Iterator[str]:
for net in self.data:
if not net.external:
continue
yield str(net)[:-6]

View File

@@ -1,8 +1,9 @@
from docker_compose.compose.net_args_yaml import NetArgsYaml from typing import NotRequired, TypedDict
# class NetYaml(TypedDict):
# internal: NotRequired[NetArgsYaml] class NetArgsYaml(TypedDict):
# proxy: NotRequired[NetArgsYaml] name: str
external: NotRequired[bool]
type NetYaml = dict[str, NetArgsYaml] type NetYaml = dict[str, NetArgsYaml]

View File

@@ -0,0 +1,54 @@
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from typing import final
from docker_compose.cfg import ROOT
from docker_compose.compose.compose import Compose
@final
@dataclass(slots=True)
class Rendered(Compose):
# @property
# def org(self) -> str:
# return self.cfg.org_data.org
@property
def bind_vols(self) -> Iterator[Path]:
root = str(ROOT)
for app in self.services:
for vol in app.volumes:
path = self.cfg.render_all(vol.split(":", 1)[0])
if not path.startswith(root):
continue
path = Path(path)
if path.is_file():
continue
yield path
# @property
# def missing_bind_vols(self) -> Iterator[Path]:
# for path in self.bind_vols:
# if path.exists():
# continue
# yield path
def mk_bind_vols(self) -> None:
for path in self.bind_vols:
path.mkdir(parents=True, exist_ok=True)
def write_bind_vols(self) -> None:
self.cfg.src_paths.volume_data.write(map(str, self.bind_vols))
@property
def proxy_nets(self) -> Iterator[str]:
for net in self.networks.proxys:
yield self.cfg.render_all(net)
@property
def as_rendered(self) -> str:
return self.cfg.render(self.as_template)
def write(self) -> None:
self.cfg.dest_paths.compose_file.write(self.as_rendered)

View File

@@ -0,0 +1,102 @@
from dataclasses import dataclass
from typing import Self, final, override
from docker_compose.cfg.compose_paths import ServicePath, ServiceVal
from docker_compose.cfg.org import OrgData, UrlVal
from docker_compose.compose.services_yaml import (
HealthCheck,
ServiceYamlRead,
ServiceYamlWrite,
)
from docker_compose.util.Ts import T_Primitive
@final
@dataclass(frozen=True, slots=True)
class Service:
_traefik_labels = frozenset(
(
f"traefik.http.routers.{OrgData.org_app.old!s}.rule=Host(`{UrlVal.old!s}`)",
f"traefik.http.routers.{OrgData.org_app.old!s}.entrypoints=websecure",
f"traefik.docker.network={OrgData.org_app.old!s}_proxy",
f"traefik.http.routers.{OrgData.org_app.old!s}.tls.certresolver=le",
)
)
@override
def __hash__(self) -> int:
return hash(self.service_name)
@property
def service_name(self) -> str:
return self.service_val.new.val.stem
_sec_opts = frozenset(("no-new-privileges:true",))
# service_name: str
service_val: ServiceVal
command: tuple[str, ...]
entrypoint: tuple[str, ...]
environment: dict[str, T_Primitive]
image: str
labels: frozenset[str]
logging: dict[str, str]
networks: frozenset[str]
restart: str
security_opt: frozenset[str]
user: str | None
volumes: frozenset[str]
shm_size: str | None
depends_on: frozenset[str]
healthcheck: HealthCheck | None
ports: frozenset[str]
@classmethod
def from_path(cls, path: ServicePath) -> Self:
return cls.from_dict(path.replace, path.as_dict)
@classmethod
def from_dict(cls, service_val: ServiceVal, data: ServiceYamlRead) -> Self:
# helper = ServiceYamlProps(data)
labels = frozenset(data.get("labels", ()))
# ports = (f'"{p}"' for p in data.get("ports", ()))
return cls(
service_val,
tuple(data.get("command", ())),
tuple(data.get("entrypoint", ())),
data.get("environment", {}),
data["image"],
cls._traefik_labels.union(labels)
if "traefik.enable=true" in labels
else labels,
data.get("logging", {}),
frozenset(data.get("networks", ())),
"unless-stopped",
cls._sec_opts.union(data.get("security_opt", [])),
data.get("user"),
frozenset(data.get("volumes", ())),
data.get("shm_size"),
frozenset(data.get("depends_on", ())),
data.get("healthcheck"),
frozenset(data.get("ports", ())),
)
@property
def as_dict(self) -> ServiceYamlWrite:
return ServiceYamlWrite(
command=self.command,
entrypoint=self.entrypoint,
environment=self.environment,
image=self.image,
labels=self.labels,
logging=self.logging,
networks=self.networks,
security_opt=self.security_opt,
user=self.user,
volumes=self.volumes,
container_name=self.service_val(ServicePath.fqdn.new),
restart=self.restart,
shm_size=self.shm_size,
depends_on=self.depends_on,
healthcheck=self.healthcheck,
ports=self.ports,
)

View File

@@ -0,0 +1,47 @@
from typing import NotRequired, TypedDict
from docker_compose.util.Ts import T_Primitive
class HealthCheck(TypedDict):
test: list[str] | str
interval: NotRequired[str]
timeout: NotRequired[str]
retries: NotRequired[int]
start_period: NotRequired[str]
class ServiceYamlRead(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]]
shm_size: NotRequired[str]
depends_on: NotRequired[list[str]]
healthcheck: NotRequired[HealthCheck]
ports: NotRequired[list[str]]
class ServiceYamlWrite(TypedDict):
command: tuple[str, ...]
entrypoint: tuple[str, ...]
environment: dict[str, T_Primitive]
image: str
labels: frozenset[str]
logging: dict[str, str]
networks: frozenset[str]
security_opt: frozenset[str]
user: str | None
volumes: frozenset[str]
container_name: str
restart: str
shm_size: str | None
depends_on: frozenset[str]
healthcheck: HealthCheck | None
ports: frozenset[str]

View File

@@ -0,0 +1,3 @@
from docker_compose.util.Ts import T_YamlDict
type VolYaml = dict[str, T_YamlDict]

View File

View File

@@ -0,0 +1,99 @@
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,
)
class TypedYamlDict[K: object, V: object](Protocol):
def __getitem__(self, key: str | K, /) -> V: ...
# 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 __len__(self) -> int: ...
def keys(self) -> KeysView[K]: ...
def items(self) -> ItemsView[K, V]: ...
def pop(self, key: Never | K, /) -> V: ...
# def popitem(self) -> tuple[K, V]: ...
# def clear(self) -> None: ...
__required_keys__: ClassVar[frozenset[str]]
__optional_keys__: ClassVar[frozenset[str]]
# class Test(TypedDict):
# var: str
#
#
# x = Test(var="test")
#
#
# def is_typed_dict_test(obj: TypedYamlDict[object, object]) -> None:
# print(obj)
# pass
#
#
# 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]
type T_Prim = T_Primitive | T_PrimIters | T_PrimDict
type T_YamlIters = tuple[T_Yaml, ...] | list[T_Yaml] | Set[T_Yaml] | Iterator[T_Yaml]
type T_YamlDict = MutableMapping[str, T_Yaml]
type T_YamlRW = T_YamlIters | T_YamlDict
type T_Yaml = T_Primitive | T_YamlRW
type T_YamlPostDict = TypedYamlDict[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]
if isinstance(annotation, TypeAliasType):
annotation = annotation.__value__ # pyright: ignore[reportAny]
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
def get_types(
annotation: TypeAliasType | GenericAlias | UnionType,
) -> type | tuple[type, ...]:
if isinstance(annotation, TypeAliasType):
annotation = annotation.__value__ # pyright: ignore[reportAny]
if isinstance(annotation, GenericAlias):
return get_origin(annotation)
if isinstance(annotation, UnionType):
return tuple(get_union_types(annotation))
return cast(type, annotation) # pyright: ignore[reportInvalidCast]

View File

@@ -0,0 +1,29 @@
# from collections.abc import Iterator, Mapping
# from typing import Any, cast
#
# from docker_compose.util.Ts import T_PrimDict, T_Primitive, T_PrimVal
#
#
# def merge_dicts[T: Mapping[Any, Any]](dict1: T, dict2: T) -> T:
# def _merge_dicts(
# _dict1: T_PrimDict, _dict2: T_PrimDict
# ) -> Iterator[tuple[T_Primitive, T_PrimVal]]:
# s1 = frozenset(_dict1.keys())
# s2 = frozenset(_dict2.keys())
# for k in s1.difference(s2):
# yield k, _dict1[k]
# for k in s2.difference(s1):
# yield k, _dict2[k]
# for k in s1.intersection(s2):
# v1 = _dict1[k]
# v2 = _dict2[k]
# if isinstance(v1, dict) and isinstance(v2, dict):
# yield k, dict[T_Primitive, T_PrimVal](_merge_dicts(v1, v2))
# continue
# if isinstance(v1, list) and isinstance(v2, list):
# yield k, list(frozenset(v1).union(v2))
# continue
# raise Exception("merge error")
#
# return cast(T, dict(_merge_dicts(dict1, dict2)))
#

View File

@@ -0,0 +1,157 @@
import re
from collections.abc import Iterator, MutableMapping, Set
from pathlib import Path
from typing import cast, get_type_hints, is_typeddict, override
import yaml
from docker_compose.util.Ts import (
T_YamlDict,
T_YamlIters,
T_YamlPost,
T_YamlPostDict,
T_YamlPostRes,
T_YamlRW,
TypedYamlDict,
get_types,
)
# class TypedYamlDict[K: object, V: object](Protocol):
# def __getitem__(self, key: K, /) -> V: ...
# # def __setitem__(self, key: K, value: V, /) -> V: ...
# def __delitem__(self, key: K, /) -> V: ...
# def __contains__(self, key: K, /) -> bool: ...
# def __iter__(self) -> Iterator[K]: ...
# def __len__(self) -> int: ...
# def keys(self) -> KeysView[K]: ...
# def items(self) -> ItemsView[K, V]: ...
# def pop(self, key: K, /) -> V: ...
#
# # def popitem(self) -> tuple[K, V]: ...
#
# # def clear(self) -> None: ...
#
# __required_keys__: ClassVar[frozenset[str]]
# __optional_keys__: ClassVar[frozenset[str]]
class VerboseSafeDumper(yaml.SafeDumper):
@override
def ignore_aliases(self, data: object) -> bool:
return True
def yaml_prep(data: T_YamlRW) -> T_YamlPostRes:
if isinstance(data, MutableMapping):
return dict_prep(data)
if isinstance(data, (tuple, list)):
return tuple(list_prep(data))
res = tuple(list_prep(data))
try:
return tuple(sorted(res)) # pyright: ignore[reportArgumentType, reportUnknownArgumentType, reportUnknownVariableType]
except TypeError:
return res
def list_prep(data: T_YamlIters) -> Iterator[T_YamlPost]:
for v in data:
if isinstance(v, (MutableMapping, tuple, list, Set, Iterator)):
yield yaml_prep(v)
continue
if v:
yield v
continue
if isinstance(v, bool):
yield v
continue
def dict_prep(data: T_YamlDict) -> T_YamlPostDict:
keys = tuple(data.keys())
for k in keys:
v = data[k]
if isinstance(v, (MutableMapping, tuple, list, Set, Iterator)):
data[k] = v = yaml_prep(v) # pyright: ignore[reportArgumentType]
if v:
continue
if isinstance(v, bool):
continue
del data[k]
return cast(T_YamlPostDict, cast(object, data))
def to_yaml(data: T_YamlRW) -> str:
dict_ = yaml_prep(data)
res = yaml.dump(dict_, Dumper=VerboseSafeDumper)
res = re.sub(r"(^\s?-)", r" \g<1>", res, flags=re.MULTILINE)
return re.sub(r"(\W*?)(\d+:\d+)", r'\g<1>"\g<2>"', res, flags=re.MULTILINE)
def write_yaml(
data: T_YamlRW,
path: Path,
) -> None:
with path.open("wt") as f:
_ = f.write(to_yaml(data))
def read_yaml(path: Path) -> T_YamlPostRes:
with path.open("rt") as f:
return yaml.safe_load(f) # pyright: ignore[reportAny]
def read_typed_yaml[T: TypedYamlDict[object, object]](
type_: type[T],
path: Path,
) -> T:
with path.open("rt") as f:
data: T_YamlDict = yaml.safe_load(f) # pyright: ignore[reportAny]
path_to_typed(type_, data, path)
return cast(T, data) # pyright: ignore[reportInvalidCast]
def path_to_typed(
type_: type[TypedYamlDict[object, object]],
data: T_YamlDict,
path: Path,
) -> None:
try:
validate_typed_dict(type_, data)
except (KeyError, TypeError) as e:
e.add_note(f"path: {path!s}")
raise e
def validate_typed_dict(
t: type[TypedYamlDict[object, object]],
data: T_YamlDict,
) -> None:
keys = frozenset(data.keys())
missing = t.__required_keys__.difference(keys)
if missing:
raise KeyError(f"missing required key(s): {', '.join(missing)}")
extra = keys.difference(t.__required_keys__, t.__optional_keys__)
if extra:
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]
continue
# try:
if not isinstance(val, get_types(t2)): # pyright: ignore[reportAny]
raise TypeError(
f"key: {key} expected *{type(t2).__name__}*, got *{type(val).__name__}*" # pyright: ignore[reportAny]
)
# valid = isinstance(val, get_types(t2))
# except TypeError:
# valid = isinstance(val, get_origin(t2))
# if not valid:
# raise TypeError(
# f"key: {key} expected *{type(t2).__name__}*, got *{type(val).__name__}*"
# )
# yield key, val

78
uv.lock generated Normal file → Executable file
View File

@@ -4,31 +4,33 @@ requires-python = ">=3.13"
[[package]] [[package]]
name = "basedpyright" name = "basedpyright"
version = "1.36.1" version = "1.37.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nodejs-wheel-binaries" }, { name = "nodejs-wheel-binaries" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/32/29/d42d543a1637e692ac557bfc6d6fcf50e9a7061c1cb4da403378d6a70453/basedpyright-1.36.1.tar.gz", hash = "sha256:20c9a24e2a4c95d5b6d46c78a6b6c7e3dc7cbba227125256431d47c595b15fd4", size = 22834851, upload-time = "2025-12-11T14:55:47.463Z" } sdist = { url = "https://files.pythonhosted.org/packages/60/d7/9476af6f45a70e8d23045ec59d99c2698513b7395283cadc75caeeea2b83/basedpyright-1.37.0.tar.gz", hash = "sha256:affbffced97a04a08bfc44aef2da43951a5ab5e2e55921a144ed786c4fd2c6ad", size = 22837441, upload-time = "2026-01-04T09:59:32.652Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/7f/f0133313bffa303d32aa74468981eb6b2da7fadda6247c9aa0aeab8391b1/basedpyright-1.36.1-py3-none-any.whl", hash = "sha256:3d738484fe9681cdfe35dd98261f30a9a7aec64208bc91f8773a9aaa9b89dd16", size = 11881725, upload-time = "2025-12-11T14:55:43.805Z" }, { url = "https://files.pythonhosted.org/packages/e8/63/753918f0bad07a1b24755a540b64bca1388322615025d4c954e3740fcdbe/basedpyright-1.37.0-py3-none-any.whl", hash = "sha256:261a02a8732a19f3f585e2940582147560058626a062a2320724de84fb2dc41b", size = 11884509, upload-time = "2026-01-04T09:59:35.997Z" },
] ]
[[package]] [[package]]
name = "compose" name = "docker-compose"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "basedpyright" }, { name = "basedpyright" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "basedpyright", specifier = ">=1.36.1" }, { name = "basedpyright", specifier = ">=1.37.0" },
{ name = "pyyaml", specifier = ">=6.0.3" }, { name = "pyyaml", specifier = ">=6.0.3" },
{ name = "ruff", specifier = ">=0.14.9" }, { name = "ruff", specifier = ">=0.14.10" },
{ name = "ty", specifier = ">=0.0.10" },
] ]
[[package]] [[package]]
@@ -85,26 +87,50 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.14.9" version = "0.14.10"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/1b/ab712a9d5044435be8e9a2beb17cbfa4c241aa9b5e4413febac2a8b79ef2/ruff-0.14.9.tar.gz", hash = "sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b", size = 5809165, upload-time = "2025-12-11T21:39:47.381Z" } sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/1c/d1b1bba22cffec02351c78ab9ed4f7d7391876e12720298448b29b7229c1/ruff-0.14.9-py3-none-linux_armv6l.whl", hash = "sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75", size = 13576541, upload-time = "2025-12-11T21:39:14.806Z" }, { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" },
{ url = "https://files.pythonhosted.org/packages/94/ab/ffe580e6ea1fca67f6337b0af59fc7e683344a43642d2d55d251ff83ceae/ruff-0.14.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2", size = 13779363, upload-time = "2025-12-11T21:39:20.29Z" }, { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" },
{ url = "https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c", size = 12925292, upload-time = "2025-12-11T21:39:38.757Z" }, { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697", size = 13362894, upload-time = "2025-12-11T21:39:02.524Z" }, { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" },
{ url = "https://files.pythonhosted.org/packages/31/1c/5b4e8e7750613ef43390bb58658eaf1d862c0cc3352d139cd718a2cea164/ruff-0.14.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27", size = 13311482, upload-time = "2025-12-11T21:39:17.51Z" }, { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" },
{ url = "https://files.pythonhosted.org/packages/5b/3a/459dce7a8cb35ba1ea3e9c88f19077667a7977234f3b5ab197fad240b404/ruff-0.14.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648", size = 14016100, upload-time = "2025-12-11T21:39:41.948Z" }, { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" },
{ url = "https://files.pythonhosted.org/packages/a6/31/f064f4ec32524f9956a0890fc6a944e5cf06c63c554e39957d208c0ffc45/ruff-0.14.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743", size = 15477729, upload-time = "2025-12-11T21:39:23.279Z" }, { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" },
{ url = "https://files.pythonhosted.org/packages/7a/6d/f364252aad36ccd443494bc5f02e41bf677f964b58902a17c0b16c53d890/ruff-0.14.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb", size = 15122386, upload-time = "2025-12-11T21:39:33.125Z" }, { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" },
{ url = "https://files.pythonhosted.org/packages/20/02/e848787912d16209aba2799a4d5a1775660b6a3d0ab3944a4ccc13e64a02/ruff-0.14.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273", size = 14497124, upload-time = "2025-12-11T21:38:59.33Z" }, { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" },
{ url = "https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a", size = 14195343, upload-time = "2025-12-11T21:39:44.866Z" }, { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" },
{ url = "https://files.pythonhosted.org/packages/f6/53/3bb8d2fa73e4c2f80acc65213ee0830fa0c49c6479313f7a68a00f39e208/ruff-0.14.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed", size = 14346425, upload-time = "2025-12-11T21:39:05.927Z" }, { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" },
{ url = "https://files.pythonhosted.org/packages/ad/04/bdb1d0ab876372da3e983896481760867fc84f969c5c09d428e8f01b557f/ruff-0.14.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b", size = 13258768, upload-time = "2025-12-11T21:39:08.691Z" }, { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" },
{ url = "https://files.pythonhosted.org/packages/40/d9/8bf8e1e41a311afd2abc8ad12be1b6c6c8b925506d9069b67bb5e9a04af3/ruff-0.14.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567", size = 13326939, upload-time = "2025-12-11T21:39:53.842Z" }, { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" },
{ url = "https://files.pythonhosted.org/packages/f4/56/a213fa9edb6dd849f1cfbc236206ead10913693c72a67fb7ddc1833bf95d/ruff-0.14.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a", size = 13578888, upload-time = "2025-12-11T21:39:35.988Z" }, { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" },
{ url = "https://files.pythonhosted.org/packages/33/09/6a4a67ffa4abae6bf44c972a4521337ffce9cbc7808faadede754ef7a79c/ruff-0.14.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8", size = 14314473, upload-time = "2025-12-11T21:39:50.78Z" }, { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" },
{ url = "https://files.pythonhosted.org/packages/12/0d/15cc82da5d83f27a3c6b04f3a232d61bc8c50d38a6cd8da79228e5f8b8d6/ruff-0.14.9-py3-none-win32.whl", hash = "sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197", size = 13202651, upload-time = "2025-12-11T21:39:26.628Z" }, { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" },
{ url = "https://files.pythonhosted.org/packages/32/f7/c78b060388eefe0304d9d42e68fab8cffd049128ec466456cef9b8d4f06f/ruff-0.14.9-py3-none-win_amd64.whl", hash = "sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2", size = 14702079, upload-time = "2025-12-11T21:39:11.954Z" }, { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" },
{ url = "https://files.pythonhosted.org/packages/26/09/7a9520315decd2334afa65ed258fed438f070e31f05a2e43dd480a5e5911/ruff-0.14.9-py3-none-win_arm64.whl", hash = "sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84", size = 13744730, upload-time = "2025-12-11T21:39:29.659Z" }, { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
]
[[package]]
name = "ty"
version = "0.0.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/85/97b5276baa217e05db2fe3d5c61e4dfd35d1d3d0ec95bfca1986820114e0/ty-0.0.10.tar.gz", hash = "sha256:0a1f9f7577e56cd508a8f93d0be2a502fdf33de6a7d65a328a4c80b784f4ac5f", size = 4892892, upload-time = "2026-01-07T23:00:23.572Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/7a/5a7147ce5231c3ccc55d6f945dabd7412e233e755d28093bfdec988ba595/ty-0.0.10-py3-none-linux_armv6l.whl", hash = "sha256:406a8ea4e648551f885629b75dc3f070427de6ed099af45e52051d4c68224829", size = 9835881, upload-time = "2026-01-07T22:08:17.492Z" },
{ url = "https://files.pythonhosted.org/packages/3e/7d/89f4d2277c938332d047237b47b11b82a330dbff4fff0de8574cba992128/ty-0.0.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d6e0a733e3d6d3bce56d6766bc61923e8b130241088dc2c05e3c549487190096", size = 9696404, upload-time = "2026-01-07T22:08:37.965Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cd/9dd49e6d40e54d4b7d563f9e2a432c4ec002c0673a81266e269c4bc194ce/ty-0.0.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e4832f8879cb95fc725f7e7fcab4f22be0cf2550f3a50641d5f4409ee04176d4", size = 9181195, upload-time = "2026-01-07T22:59:07.187Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b8/3e7c556654ba0569ed5207138d318faf8633d87e194760fc030543817c26/ty-0.0.10-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:6b58cc78e5865bc908f053559a80bb77cab0dc168aaad2e88f2b47955694b138", size = 9665002, upload-time = "2026-01-07T22:08:30.782Z" },
{ url = "https://files.pythonhosted.org/packages/98/96/410a483321406c932c4e3aa1581d1072b72cdcde3ae83cd0664a65c7b254/ty-0.0.10-py3-none-manylinux_2_24_armv7l.whl", hash = "sha256:83c6a514bb86f05005fa93e3b173ae3fde94d291d994bed6fe1f1d2e5c7331cf", size = 9664948, upload-time = "2026-01-07T23:04:14.655Z" },
{ url = "https://files.pythonhosted.org/packages/1f/5d/cba2ab3e2f660763a72ad12620d0739db012e047eaa0ceaa252bf5e94ebb/ty-0.0.10-py3-none-manylinux_2_24_i686.whl", hash = "sha256:2e43f71e357f8a4f7fc75e4753b37beb2d0f297498055b1673a9306aa3e21897", size = 10125401, upload-time = "2026-01-07T22:08:28.171Z" },
{ url = "https://files.pythonhosted.org/packages/a7/67/29536e0d97f204a2933122239298e754db4564f4ed7f34e2153012b954be/ty-0.0.10-py3-none-manylinux_2_24_ppc64le.whl", hash = "sha256:18be3c679965c23944c8e574be0635504398c64c55f3f0c46259464e10c0a1c7", size = 10714052, upload-time = "2026-01-07T22:08:20.098Z" },
{ url = "https://files.pythonhosted.org/packages/63/c8/82ac83b79a71c940c5dcacb644f526f0c8fdf4b5e9664065ab7ee7c0e4ec/ty-0.0.10-py3-none-manylinux_2_24_s390x.whl", hash = "sha256:5477981681440a35acdf9b95c3097410c547abaa32b893f61553dbc3b0096fff", size = 10395924, upload-time = "2026-01-07T22:08:22.839Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4c/2f9ac5edbd0e67bf82f5cd04275c4e87cbbf69a78f43e5dcf90c1573d44e/ty-0.0.10-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:e206a23bd887574302138b33383ae1edfcc39d33a06a12a5a00803b3f0287a45", size = 10220096, upload-time = "2026-01-07T22:08:13.171Z" },
{ url = "https://files.pythonhosted.org/packages/04/13/3be2b7bfd53b9952b39b6f2c2ef55edeb1a2fea3bf0285962736ee26731c/ty-0.0.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4e09ddb0d3396bd59f645b85eab20f9a72989aa8b736b34338dcb5ffecfe77b6", size = 9649120, upload-time = "2026-01-07T22:08:34.003Z" },
{ url = "https://files.pythonhosted.org/packages/93/e3/edd58547d9fd01e4e584cec9dced4f6f283506b422cdd953e946f6a8e9f0/ty-0.0.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:139d2a741579ad86a044233b5d7e189bb81f427eebce3464202f49c3ec0eba3b", size = 9686033, upload-time = "2026-01-07T22:08:40.967Z" },
{ url = "https://files.pythonhosted.org/packages/cc/bc/9d2f5fec925977446d577fb9b322d0e7b1b1758709f23a6cfc10231e9b84/ty-0.0.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6bae10420c0abfe4601fbbc6ce637b67d0b87a44fa520283131a26da98f2e74c", size = 9841905, upload-time = "2026-01-07T23:04:21.694Z" },
{ url = "https://files.pythonhosted.org/packages/7c/b8/5acd3492b6a4ef255ace24fcff0d4b1471a05b7f3758d8910a681543f899/ty-0.0.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7358bbc5d037b9c59c3a48895206058bcd583985316c4125a74dd87fd1767adb", size = 10320058, upload-time = "2026-01-07T22:08:25.645Z" },
{ url = "https://files.pythonhosted.org/packages/35/67/5b6906fccef654c7e801d6ac8dcbe0d493e1f04c38127f82a5e6d7e0aa0e/ty-0.0.10-py3-none-win32.whl", hash = "sha256:f51b6fd485bc695d0fdf555e69e6a87d1c50f14daef6cb980c9c941e12d6bcba", size = 9271806, upload-time = "2026-01-07T22:08:10.08Z" },
{ url = "https://files.pythonhosted.org/packages/42/36/82e66b9753a76964d26fd9bc3514ea0abce0a5ba5ad7d5f084070c6981da/ty-0.0.10-py3-none-win_amd64.whl", hash = "sha256:16deb77a72cf93b89b4d29577829613eda535fbe030513dfd9fba70fe38bc9f5", size = 10130520, upload-time = "2026-01-07T23:04:11.759Z" },
{ url = "https://files.pythonhosted.org/packages/63/52/89da123f370e80b587d2db8551ff31562c882d87b32b0e92b59504b709ae/ty-0.0.10-py3-none-win_arm64.whl", hash = "sha256:7495288bca7afba9a4488c9906466d648ffd3ccb6902bc3578a6dbd91a8f05f0", size = 9626026, upload-time = "2026-01-07T23:04:17.91Z" },
] ]