sync
This commit is contained in:
174
.gitignore
vendored
174
.gitignore
vendored
@@ -1,176 +1,10 @@
|
|||||||
# ---> Python
|
# Python-generated files
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[oc]
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
wheels/
|
||||||
share/python-wheels/
|
*.egg-info
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
# Virtual environments
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# UV
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
#uv.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
.venv
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
# Ruff stuff:
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# PyPI configuration file
|
|
||||||
.pypirc
|
|
||||||
|
|
||||||
|
|||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
20
.vscode/settings.json
vendored
20
.vscode/settings.json
vendored
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"python.languageServer": "None",
|
"python.languageServer": "None",
|
||||||
"cSpell.words": [
|
|
||||||
"traefik"
|
|
||||||
],
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"[python]": {
|
"[python]": {
|
||||||
|
"editor.formatOnType": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.organizeImports": "explicit",
|
"source.fixAll.ruff": "explicit",
|
||||||
"source.unusedImports": "explicit"
|
"source.organizeImports.ruff": "explicit"
|
||||||
}
|
},
|
||||||
},
|
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||||
"terminal.integrated.env.linux": {
|
|
||||||
"PYTHONPATH": "${workspaceFolder}/src"
|
|
||||||
},
|
},
|
||||||
|
"cSpell.words": [
|
||||||
|
"certresolver",
|
||||||
|
"traefik",
|
||||||
|
"websecure"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
13
docs/workflow.md
Normal file
13
docs/workflow.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
read --> src_paths
|
||||||
|
src_paths --> cfg
|
||||||
|
cfg --> parsed
|
||||||
|
cfg --> template_args
|
||||||
|
template_args --> dest_paths
|
||||||
|
src_paths --> template
|
||||||
|
parsed --> template
|
||||||
|
dest_paths --> template
|
||||||
|
template --> write
|
||||||
|
|
||||||
|
```
|
||||||
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[project]
|
||||||
|
name = "compose"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [{ name = "Christian Camper", email = "ccamper7@gmail.com" }]
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"basedpyright>=1.36.1",
|
||||||
|
"pyyaml>=6.0.3",
|
||||||
|
"ruff>=0.14.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
compose = "compose:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.9.17,<0.10.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
26
src/cfg/__init__.py
Normal file
26
src/cfg/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
type nested_list = list[str | nested_list]
|
||||||
|
type T_Primitive = bool | int | str
|
||||||
|
type T_PrimVal = T_Primitive | list[T_Primitive] | T_PrimDict
|
||||||
|
type T_PrimDict = Mapping[T_Primitive, T_PrimVal]
|
||||||
|
type T_YamlVals = T_Primitive | list[T_Primitive | T_YamlDict] | T_YamlDict
|
||||||
|
type T_YamlDict = Mapping[str, T_YamlVals]
|
||||||
|
|
||||||
|
CFG_ROOT = Path("/data/cfg")
|
||||||
|
DATA_ROOT = Path("/data")
|
||||||
|
TRAEFIK_PATH = Path("/data/traefik")
|
||||||
|
|
||||||
|
# TCo_YamlVals = TypeVar(
|
||||||
|
# "TCo_YamlVals",
|
||||||
|
# bound=T_Primitive | list[T_Primitive | T_YamlDict] | T_YamlDict,
|
||||||
|
# covariant=True,
|
||||||
|
# )
|
||||||
|
# type TCo_YamlDict = dict[str, TCo_YamlVals]
|
||||||
|
|
||||||
|
# TCo_YamlDict = TypeVar("TCo_YamlDict", bound=dict[str, T_YamlVals], covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
# class HasServices(TypedDict):
|
||||||
|
# services: dict[str, ComposeService]
|
||||||
32
src/cfg/entity.py
Normal file
32
src/cfg/entity.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import NotRequired, TypedDict, final
|
||||||
|
|
||||||
|
from src_path.entity import SrcPaths
|
||||||
|
|
||||||
|
|
||||||
|
class OrgDataYaml(TypedDict):
|
||||||
|
org: NotRequired[str]
|
||||||
|
url: NotRequired[str]
|
||||||
|
|
||||||
|
|
||||||
|
class CfgDataYaml(TypedDict):
|
||||||
|
name: str
|
||||||
|
files: list[str]
|
||||||
|
orgs: list[OrgDataYaml]
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class OrgData:
|
||||||
|
org: str | None
|
||||||
|
url: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class CfgData:
|
||||||
|
src_paths: SrcPaths
|
||||||
|
name: str
|
||||||
|
files: frozenset[Path]
|
||||||
|
orgs: frozenset[OrgData] | None
|
||||||
18
src/cfg/factory.py
Normal file
18
src/cfg/factory.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from cfg.entity import CfgData, CfgDataYaml
|
||||||
|
from cfg.get import cfg_get_orgs
|
||||||
|
from src_path.entity import SrcPaths
|
||||||
|
from src_path.get import src_path_get_files
|
||||||
|
from util import read_yml
|
||||||
|
|
||||||
|
|
||||||
|
def cfg_data_factory(cfg_dir: SrcPaths) -> CfgData:
|
||||||
|
data = cast(CfgDataYaml, read_yml(cfg_dir.cfg_file))
|
||||||
|
|
||||||
|
return CfgData(
|
||||||
|
cfg_dir,
|
||||||
|
data["name"],
|
||||||
|
frozenset(src_path_get_files(cfg_dir, data)),
|
||||||
|
frozenset(cfg_get_orgs(data)),
|
||||||
|
)
|
||||||
20
src/cfg/get.py
Normal file
20
src/cfg/get.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from collections.abc import Iterator
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from cfg.entity import CfgData, CfgDataYaml, OrgData
|
||||||
|
from service.entity import Service, ServiceYaml
|
||||||
|
from util import read_yml
|
||||||
|
|
||||||
|
|
||||||
|
def cfg_get_orgs(data: CfgDataYaml) -> Iterator[OrgData]:
|
||||||
|
for org_data in data["orgs"]:
|
||||||
|
yield OrgData(
|
||||||
|
org_data.get("org"),
|
||||||
|
org_data.get("url"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cfg_get_services(cfg_data: CfgData) -> Iterator[tuple[str, Service]]:
|
||||||
|
for path in cfg_data.files:
|
||||||
|
_dict = cast(ServiceYaml, read_yml(path))
|
||||||
|
yield path.stem, Service.from_dict(_dict)
|
||||||
250
src/compose.py
250
src/compose.py
@@ -1,250 +0,0 @@
|
|||||||
from collections.abc import Collection, Iterator
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from functools import reduce
|
|
||||||
from pathlib import Path
|
|
||||||
from shutil import copyfile
|
|
||||||
from typing import Literal, NotRequired, Protocol, cast, final
|
|
||||||
|
|
||||||
|
|
||||||
from Ts import (
|
|
||||||
CfgData,
|
|
||||||
Compose,
|
|
||||||
ComposeNet,
|
|
||||||
ComposeNetArgs,
|
|
||||||
HasServices,
|
|
||||||
OrgData,
|
|
||||||
TraefikComposeDict,
|
|
||||||
TraefikNet,
|
|
||||||
TraefikNetName,
|
|
||||||
)
|
|
||||||
from util import merge_dicts, read_yml, to_yaml
|
|
||||||
from val_objs import RecordCls
|
|
||||||
|
|
||||||
# CFG_ROOT = Path("/data/cfg")
|
|
||||||
|
|
||||||
|
|
||||||
# def replace(rec: RecordCls, string: str) -> str:
|
|
||||||
# return str.replace(string, rec.name, rec.val.to_str())
|
|
||||||
|
|
||||||
|
|
||||||
# @final
|
|
||||||
# @dataclass(frozen=True, slots=True)
|
|
||||||
# class ComposeBuild:
|
|
||||||
# root_path: Path
|
|
||||||
# # def __init__(self, path: str) -> None:
|
|
||||||
# # self.env_path = self.path.joinpath(".env")
|
|
||||||
|
|
||||||
|
|
||||||
# def compose_build_factory(root_path: str):
|
|
||||||
# path = Path("/data/cfg").joinpath(root_path)
|
|
||||||
# return ComposeBuild(path)
|
|
||||||
# CFG_PATH = Path("/data/cfg")
|
|
||||||
|
|
||||||
|
|
||||||
def compose_treafik(proxy_data: Iterator[tuple[str, str]]):
|
|
||||||
root_path = CFG_PATH.joinpath("treafik")
|
|
||||||
cfg_data = CfgData(
|
|
||||||
name="treafik",
|
|
||||||
files=["treafik.yml"],
|
|
||||||
orgs=[OrgData(org="util", url="treafik")],
|
|
||||||
)
|
|
||||||
services = build_compose(root_path, cfg_data)["services"]
|
|
||||||
networks: TraefikNet = dict()
|
|
||||||
for name, proxy in proxy_data:
|
|
||||||
networks[name] = TraefikNetName(name=proxy)
|
|
||||||
traefik_compose = TraefikComposeDict(
|
|
||||||
name="traefik",
|
|
||||||
services=services,
|
|
||||||
networks=networks,
|
|
||||||
)
|
|
||||||
mk_volumes = tuple(get_volumes(traefik_compose))
|
|
||||||
og_yaml = to_yaml(traefik_compose)
|
|
||||||
org = cfg_data["orgs"][0]
|
|
||||||
|
|
||||||
|
|
||||||
# def build_compose(compose_rw: ComposeRWData, cfg_data: CfgData) -> Compose:
|
|
||||||
# compose_dict = get_compose_dict(compose_rw, cfg_data)
|
|
||||||
# insert_defaults(compose_dict)
|
|
||||||
# insert_traefik_labels(compose_dict)
|
|
||||||
# return compose_dict
|
|
||||||
|
|
||||||
|
|
||||||
# def write_compose(write_data: ComposeData):
|
|
||||||
# write(write_data)
|
|
||||||
# mk_compose_dir(write_data)
|
|
||||||
# mk_compose_env(write_data.rw)
|
|
||||||
|
|
||||||
|
|
||||||
# def gen_compose(path: str) -> Iterator[tuple[str, str]]:
|
|
||||||
# # compose_build = compose_build_factory(path)
|
|
||||||
# root_path = CFG_PATH.joinpath(path)
|
|
||||||
# cfg_data = get_config_data(root_path)
|
|
||||||
# compose_dict = build_compose(root_path, cfg_data)
|
|
||||||
# mk_volumes = tuple(get_volumes(compose_dict))
|
|
||||||
# proxy_net = set_networks(compose_dict)
|
|
||||||
# og_yaml = to_yaml(compose_dict)
|
|
||||||
|
|
||||||
# for _dict in cfg_data["orgs"]:
|
|
||||||
# compose_rw = compose_rw_factory(
|
|
||||||
# _dict.get("org"), cfg_data["name"], _dict["url"]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# write(compose_rw, og_yaml)
|
|
||||||
# mk_compose_dir(compose_rw, mk_volumes)
|
|
||||||
# mk_compose_env(root_path, compose_rw.data_dir.to_path())
|
|
||||||
# if proxy_net is None:
|
|
||||||
# continue
|
|
||||||
# for net in proxy_net:
|
|
||||||
|
|
||||||
# def sub():
|
|
||||||
# for kv in net:
|
|
||||||
# yield replace(compose_rw.org_name, kv)
|
|
||||||
|
|
||||||
# yield tuple[str, str](sub())
|
|
||||||
|
|
||||||
|
|
||||||
# def get_config_data(cfg_dir: Path) -> CfgData:
|
|
||||||
# cfg_path = cfg_dir.joinpath("cfg.yml")
|
|
||||||
# return cast(CfgData, read_yml(cfg_path))
|
|
||||||
|
|
||||||
|
|
||||||
# def get_compose_dict(compose_rw: ComposeRWData, cfg_data: CfgData) -> Compose:
|
|
||||||
# def sub():
|
|
||||||
# for file in cfg_data["files"]:
|
|
||||||
# path = compose_rw.data_dir.joinpath(file)
|
|
||||||
# yield cast(Compose, read_yml(path))
|
|
||||||
|
|
||||||
# return reduce(merge_dicts, sub(), cast(Compose, {})) # pyright: ignore[reportInvalidCast]
|
|
||||||
|
|
||||||
|
|
||||||
# @final
|
|
||||||
# class TraefikBuild:
|
|
||||||
|
|
||||||
|
|
||||||
# def insert_defaults(data: Compose) -> None:
|
|
||||||
# data["name"] = "${_ORG_NAME}"
|
|
||||||
# for app_data in data["services"].values():
|
|
||||||
# app_data["restart"] = "unless-stopped"
|
|
||||||
# sec_opts = {"no-new-privileges:true"}
|
|
||||||
# app_data["security_opt"] = list(
|
|
||||||
# sec_opts.union(app_data.get("security_opt", set()))
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
# def insert_traefik_labels(data: Compose) -> None:
|
|
||||||
# for app_data in data["services"].values():
|
|
||||||
# traefik_labels = {
|
|
||||||
# "traefik.http.routers.${_ORG_NAME}.rule=Host(`${_URL}`)",
|
|
||||||
# "traefik.http.routers.${_ORG_NAME}.entrypoints=websecure",
|
|
||||||
# "traefik.docker.network=${_ORG_NAME}_proxy",
|
|
||||||
# "traefik.http.routers.${_ORG_NAME}.tls.certresolver=le",
|
|
||||||
# }
|
|
||||||
# if "labels" not in app_data:
|
|
||||||
# continue
|
|
||||||
# if "traefik.enable=true" not in app_data["labels"]:
|
|
||||||
# continue
|
|
||||||
# app_data["labels"] = list(traefik_labels.union(app_data["labels"]))
|
|
||||||
|
|
||||||
|
|
||||||
# def get_volumes(data: HasServices) -> Iterator[str]:
|
|
||||||
# for app_data in data["services"].values():
|
|
||||||
# if "volumes" not in app_data:
|
|
||||||
# return
|
|
||||||
# for vol in app_data["volumes"]:
|
|
||||||
# if not vol.startswith(r"${_DATA}"):
|
|
||||||
# continue
|
|
||||||
# yield vol.split(":", 1)[0]
|
|
||||||
|
|
||||||
|
|
||||||
# def set_networks(data: Compose) -> tuple[str, str] | None:
|
|
||||||
# def sub() -> Iterator[Literal["proxy", "internal"]]:
|
|
||||||
# for app_data in data["services"].values():
|
|
||||||
# if "networks" not in app_data:
|
|
||||||
# continue
|
|
||||||
# yield from app_data["networks"]
|
|
||||||
|
|
||||||
# nets = set(sub())
|
|
||||||
# net = ComposeNet()
|
|
||||||
# if "proxy" in nets:
|
|
||||||
# proxy = ComposeNetArgs(name="${_ORG_NAME}_proxy", external=True)
|
|
||||||
# net["proxy"] = proxy
|
|
||||||
# ret = "${_ORG_NAME}", "${_ORG_NAME}_proxy"
|
|
||||||
# else:
|
|
||||||
# ret = None
|
|
||||||
# if "internal" in nets:
|
|
||||||
# net["internal"] = ComposeNetArgs(name="${_ORG_NAME}")
|
|
||||||
# if not net:
|
|
||||||
# return
|
|
||||||
# data["networks"] = net
|
|
||||||
# return ret
|
|
||||||
|
|
||||||
|
|
||||||
# def compose_rw_factory(org_raw: str | None, name_raw: str, sub_url: str):
|
|
||||||
# _org = OrgVal(org_raw)
|
|
||||||
# _name = NameVal(name_raw)
|
|
||||||
# # org = Record("ORG", _org)
|
|
||||||
# # name = Record("NAME", _name)
|
|
||||||
# org_name = (
|
|
||||||
# NameVal(f"{_org.to_str()}_{_name.to_str()}") if _org.is_valid() else _name
|
|
||||||
# )
|
|
||||||
# # org_name = Record("ORG_NAME", org_name)
|
|
||||||
# data_dir = Path("/data").joinpath(_org.to_str(), _name.to_str())
|
|
||||||
# # cfg_dir = CFG_PATH.joinpath(org_name.val.to_str())
|
|
||||||
# # data = Record("DATA", DataDir(data_dir))
|
|
||||||
# # url = Record("URL", Url(sub_url))
|
|
||||||
# # Path("/data").joinpath(_org.to_str(), _name.to_str())
|
|
||||||
# # compose_path = data_dir.to_path().joinpath("docker-compose.yml")
|
|
||||||
# # env_path = data_dir.to_path().joinpath(".env")
|
|
||||||
# return ComposeRWData(
|
|
||||||
# Record("ORG", _org),
|
|
||||||
# Record("NAME", _name),
|
|
||||||
# Record("ORG_NAME", org_name),
|
|
||||||
# Record("DATA", DataDir(data_dir)),
|
|
||||||
# Record("URL", Url(sub_url)),
|
|
||||||
# data_dir,
|
|
||||||
# CFG_PATH.joinpath(org_name.to_str()),
|
|
||||||
# )
|
|
||||||
|
|
||||||
# def __init__(self, org: str | None, name: str, sub_url: str) -> None:
|
|
||||||
# super().__init__()
|
|
||||||
# self._org = OrgVal(org)
|
|
||||||
# self._name = NameVal(name)
|
|
||||||
# self.org = Record("ORG", self._org)
|
|
||||||
# self.name = Record("NAME", self._name)
|
|
||||||
# org_name = (
|
|
||||||
# NameVal(f"{self.org.val.to_str()}_{self.name.val.to_str()}")
|
|
||||||
# if self._org.is_valid()
|
|
||||||
# else self._name
|
|
||||||
# )
|
|
||||||
# self.org_name = Record("ORG_NAME", org_name)
|
|
||||||
# self.data_dir = DataDir(
|
|
||||||
# self._org,
|
|
||||||
# self._name,
|
|
||||||
# )
|
|
||||||
# self.data = Record("DATA", self.data_dir)
|
|
||||||
# self.url = Record("URL", Url(sub_url))
|
|
||||||
|
|
||||||
# self.compose_path = self.data_dir.to_path().joinpath("docker-compose.yml")
|
|
||||||
# self.env_path = self.data_dir.to_path().joinpath(".env")
|
|
||||||
|
|
||||||
|
|
||||||
# def mk_compose_dir(compose_data: ComposeData) -> None:
|
|
||||||
# for vol in compose_data.volumes:
|
|
||||||
# mk_path = Path(replace(compose_data.rw.data, vol))
|
|
||||||
# if mk_path.exists():
|
|
||||||
# continue
|
|
||||||
# mk_path.mkdir(parents=True)
|
|
||||||
|
|
||||||
# def mk_compose_env(compose_data: ComposeRWData) -> None:
|
|
||||||
# root_env_path = compose_data.cfg_dir.joinpath(".env")
|
|
||||||
# dest_env_path = compose_data.data_dir.joinpath(".env")
|
|
||||||
# if root_env_path.exists() and not dest_env_path.exists():
|
|
||||||
# _ = copyfile(root_env_path, dest_env_path)
|
|
||||||
|
|
||||||
# def write(compose_data: ComposeData) -> None:
|
|
||||||
# string = reduce(lambda s, rec: replace(rec, s), compose_data.rw, compose_data.yaml)
|
|
||||||
# compose_path = compose_data.rw.data_dir.joinpath("docker-compose.yml")
|
|
||||||
# if not compose_path.exists():
|
|
||||||
# compose_path.parent.mkdir(parents=True)
|
|
||||||
# with compose_path.open("wt") as f:
|
|
||||||
# _ = f.write(string)
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
from collections.abc import Mapping
|
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
from typing import Literal, NotRequired, Self, TypedDict, final
|
|
||||||
|
|
||||||
from util import to_yaml
|
|
||||||
|
|
||||||
type nested_list = list[str | nested_list]
|
|
||||||
type T_Primitive = bool | int | str
|
|
||||||
type T_PrimVal = T_Primitive | list[T_Primitive] | T_PrimDict
|
|
||||||
type T_PrimDict = Mapping[T_Primitive, T_PrimVal]
|
|
||||||
type T_YamlVals = T_Primitive | list[T_Primitive | T_YamlDict] | T_YamlDict
|
|
||||||
type T_YamlDict = dict[str, T_YamlVals]
|
|
||||||
|
|
||||||
|
|
||||||
class ComposeNetArgsYaml(TypedDict):
|
|
||||||
name: str
|
|
||||||
external: NotRequired[bool]
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class ComposeNetArgs:
|
|
||||||
name: str
|
|
||||||
external: bool | None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: ComposeNetArgsYaml) -> Self:
|
|
||||||
return cls(
|
|
||||||
data["name"],
|
|
||||||
data.get("external"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ComposeNetYaml(TypedDict):
|
|
||||||
internal: NotRequired[ComposeNetArgsYaml]
|
|
||||||
proxy: NotRequired[ComposeNetArgsYaml]
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class ComposeNet:
|
|
||||||
internal: ComposeNetArgs | None
|
|
||||||
proxy: ComposeNetArgs | None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_class(cls, data: ComposeNetYaml) -> Self:
|
|
||||||
internal = data.get("internal")
|
|
||||||
if internal is not None:
|
|
||||||
internal = ComposeNetArgs.from_dict(internal)
|
|
||||||
proxy = data.get("proxy")
|
|
||||||
if proxy is not None:
|
|
||||||
proxy = ComposeNetArgs.from_dict(proxy)
|
|
||||||
return cls(internal, proxy)
|
|
||||||
|
|
||||||
|
|
||||||
class ComposeServiceYaml(TypedDict):
|
|
||||||
command: NotRequired[list[str]]
|
|
||||||
container_name: str
|
|
||||||
entrypoint: list[str]
|
|
||||||
environment: NotRequired[dict[str, T_Primitive]]
|
|
||||||
image: str
|
|
||||||
labels: NotRequired[list[str]]
|
|
||||||
logging: dict[str, str]
|
|
||||||
networks: NotRequired[list[Literal["proxy", "internal"]]]
|
|
||||||
restart: str
|
|
||||||
security_opt: NotRequired[list[str]]
|
|
||||||
user: NotRequired[str]
|
|
||||||
volumes: NotRequired[list[str]]
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class ComposeService:
|
|
||||||
command: list[str] | None
|
|
||||||
container_name: str | None
|
|
||||||
entrypoint: list[str]
|
|
||||||
environment: dict[str, T_Primitive] | None
|
|
||||||
image: str
|
|
||||||
labels: set[str] | None
|
|
||||||
logging: dict[str, str] | None
|
|
||||||
networks: set[Literal["proxy", "internal"]] | None
|
|
||||||
restart: str | None
|
|
||||||
security_opt: set[str] | None
|
|
||||||
user: str | None
|
|
||||||
volumes: set[str] | None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: ComposeServiceYaml) -> Self:
|
|
||||||
labels = data.get("labels")
|
|
||||||
if labels is not None:
|
|
||||||
labels = set(labels)
|
|
||||||
sec = data.get("security_opt")
|
|
||||||
if sec is not None:
|
|
||||||
sec = set(sec)
|
|
||||||
vols = data.get("volumes")
|
|
||||||
if vols is not None:
|
|
||||||
vols = set(vols)
|
|
||||||
return cls(
|
|
||||||
data.get("command"),
|
|
||||||
None, # data['container_name'],
|
|
||||||
data["entrypoint"],
|
|
||||||
data.get("environment"),
|
|
||||||
data["image"],
|
|
||||||
labels,
|
|
||||||
None,
|
|
||||||
data.get("netwoks"),
|
|
||||||
None,
|
|
||||||
sec,
|
|
||||||
data.get("user"),
|
|
||||||
vols,
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_valid(self) -> bool:
|
|
||||||
attrs = (self.container_name, self.restart, self.logging)
|
|
||||||
for attr in attrs:
|
|
||||||
if attr is None:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class ComposeYaml(TypedDict):
|
|
||||||
name: str
|
|
||||||
networks: NotRequired[ComposeNetYaml]
|
|
||||||
services: dict[str, ComposeServiceYaml]
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class Compose:
|
|
||||||
name: str
|
|
||||||
networks: ComposeNet | None
|
|
||||||
services: dict[str, ComposeService]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: ComposeYaml) -> Self:
|
|
||||||
services = dict[str, ComposeService]()
|
|
||||||
for k, v in data["services"].items():
|
|
||||||
services[k] = ComposeService.from_dict(v)
|
|
||||||
return cls(
|
|
||||||
data["name"],
|
|
||||||
None,
|
|
||||||
services,
|
|
||||||
)
|
|
||||||
|
|
||||||
def as_yaml(self) -> str:
|
|
||||||
return to_yaml(asdict(self))
|
|
||||||
|
|
||||||
|
|
||||||
class TraefikNetName(TypedDict):
|
|
||||||
name: str
|
|
||||||
|
|
||||||
|
|
||||||
type TraefikNet = dict[str, TraefikNetName]
|
|
||||||
|
|
||||||
|
|
||||||
class TraefikComposeDict(TypedDict):
|
|
||||||
name: str
|
|
||||||
networks: TraefikNet
|
|
||||||
services: dict[str, ComposeServiceYaml]
|
|
||||||
|
|
||||||
|
|
||||||
class HasServices(TypedDict):
|
|
||||||
services: dict[str, ComposeServiceYaml]
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
from collections.abc import Iterator
|
|
||||||
|
|
||||||
from .compose_struct import Compose, ComposeService, ComposeServiceYaml, ComposeYaml
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from functools import reduce
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Literal, NotRequired, Self, TypedDict, cast, final
|
|
||||||
|
|
||||||
from util import merge_dicts, read_yml, to_yaml
|
|
||||||
|
|
||||||
from .compose_struct import ComposeNet, ComposeNetArgs, HasServices
|
|
||||||
from .val_objs import DataDir, NameVal, OrgVal, Record, RecordCls, RecordVal, Url
|
|
||||||
|
|
||||||
|
|
||||||
def replace(rec: RecordCls[RecordVal], string: str) -> str:
|
|
||||||
return str.replace(string, rec.name, rec.val.to_str())
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class SrcPaths:
|
|
||||||
data_dir: Path
|
|
||||||
cfg_file: Path
|
|
||||||
env_file: Path
|
|
||||||
|
|
||||||
|
|
||||||
def src_path_factory(src: str) -> SrcPaths:
|
|
||||||
root = Path("/data/cfg")
|
|
||||||
dir = root.joinpath(src)
|
|
||||||
return SrcPaths(
|
|
||||||
data_dir=dir,
|
|
||||||
cfg_file=dir.joinpath("cfg.yml"),
|
|
||||||
env_file=dir.joinpath(".env"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OrgDataYaml(TypedDict):
|
|
||||||
org: NotRequired[str]
|
|
||||||
url: NotRequired[str]
|
|
||||||
|
|
||||||
|
|
||||||
class CfgDataYaml(TypedDict):
|
|
||||||
name: str
|
|
||||||
files: list[str]
|
|
||||||
orgs: list[OrgDataYaml]
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class OrgData:
|
|
||||||
org: str | None
|
|
||||||
url: str | None
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class CfgData:
|
|
||||||
name: str
|
|
||||||
files: tuple[Path, ...]
|
|
||||||
orgs: tuple[OrgData, ...]
|
|
||||||
|
|
||||||
|
|
||||||
def config_data_factory(cfg_dir: SrcPaths) -> CfgData:
|
|
||||||
data = cast(CfgDataYaml, read_yml(cfg_dir.cfg_file))
|
|
||||||
|
|
||||||
def sub_files():
|
|
||||||
for path in data["files"]:
|
|
||||||
yield cfg_dir.data_dir.joinpath(path)
|
|
||||||
|
|
||||||
def sub_orgs():
|
|
||||||
for org_data in data["orgs"]:
|
|
||||||
yield OrgData(
|
|
||||||
org_data.get("org"),
|
|
||||||
org_data.get("url"),
|
|
||||||
)
|
|
||||||
|
|
||||||
return CfgData(
|
|
||||||
data["name"],
|
|
||||||
tuple(sub_files()),
|
|
||||||
tuple(sub_orgs()),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class ComposeTemplate:
|
|
||||||
org: RecordCls[OrgVal]
|
|
||||||
name: RecordCls[NameVal]
|
|
||||||
org_name: RecordCls[NameVal]
|
|
||||||
data: RecordCls[DataDir]
|
|
||||||
url: RecordCls[Url]
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
yield self.org
|
|
||||||
yield self.name
|
|
||||||
yield self.org_name
|
|
||||||
yield self.data
|
|
||||||
yield self.url
|
|
||||||
|
|
||||||
|
|
||||||
def compose_template_factory(cfg_data: CfgData) -> Iterator[ComposeTemplate]:
|
|
||||||
# def sub():
|
|
||||||
for org_data in cfg_data.orgs:
|
|
||||||
_org = OrgVal(org_data.org)
|
|
||||||
_name = NameVal(cfg_data.name)
|
|
||||||
org_name = (
|
|
||||||
NameVal(f"{_org.to_str()}_{_name.to_str()}") if _org.is_valid() else _name
|
|
||||||
)
|
|
||||||
data_dir = Path("/data").joinpath(_org.to_str(), _name.to_str())
|
|
||||||
|
|
||||||
yield ComposeTemplate(
|
|
||||||
Record("ORG", _org),
|
|
||||||
Record("NAME", _name),
|
|
||||||
Record("ORG_NAME", org_name),
|
|
||||||
Record("DATA", DataDir(data_dir)),
|
|
||||||
Record("URL", Url(org_data.url)),
|
|
||||||
# data_dir,
|
|
||||||
# CFG_PATH.joinpath(org_name.to_str()),
|
|
||||||
)
|
|
||||||
|
|
||||||
# return tuple(sub())
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class DestPaths:
|
|
||||||
data_dir: Path
|
|
||||||
env_file: Path
|
|
||||||
compose_file: Path
|
|
||||||
|
|
||||||
|
|
||||||
def dest_paths_factory(compose_template: ComposeTemplate) -> DestPaths:
|
|
||||||
data_dir = compose_template.data.val.path
|
|
||||||
return DestPaths(
|
|
||||||
data_dir, data_dir.joinpath(".env"), data_dir.joinpath("docker-compose.yml")
|
|
||||||
)
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class ParsedCompose:
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class DestData:
|
|
||||||
paths: DestPaths
|
|
||||||
templates: ComposeTemplate
|
|
||||||
volumes: tuple[Path, ...]
|
|
||||||
|
|
||||||
|
|
||||||
def get_volumes_raw(data: HasServices) -> Iterator[str]:
|
|
||||||
for app_data in data["services"].values():
|
|
||||||
if "volumes" not in app_data:
|
|
||||||
return
|
|
||||||
for vol in app_data["volumes"]:
|
|
||||||
if not vol.startswith(r"${_DATA}"):
|
|
||||||
continue
|
|
||||||
yield vol.split(":", 1)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def dest_data_factory(cfg_data: CfgData, compose: Compose) -> Iterator[DestData]:
|
|
||||||
# def sub():
|
|
||||||
vols_raw = tuple(get_volumes_raw(compose))
|
|
||||||
# templates = tuple(compose_template_factory(cfg_data))
|
|
||||||
# for template in templates:
|
|
||||||
# for vol in vols_raw:
|
|
||||||
|
|
||||||
for template in compose_template_factory(cfg_data):
|
|
||||||
paths = dest_paths_factory(template)
|
|
||||||
|
|
||||||
def vol_sub():
|
|
||||||
for vol in vols_raw:
|
|
||||||
yield Path(replace(template.data.val, vol))
|
|
||||||
|
|
||||||
yield DestData(
|
|
||||||
paths,
|
|
||||||
template,
|
|
||||||
tuple(vol_sub()),
|
|
||||||
)
|
|
||||||
|
|
||||||
# return tuple(sub())
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class ComposeData:
|
|
||||||
src_paths: SrcPaths
|
|
||||||
dest_data: tuple[DestData, ...]
|
|
||||||
Compose: Compose
|
|
||||||
proxy_net: tuple[str, str] | None
|
|
||||||
yaml: str
|
|
||||||
|
|
||||||
|
|
||||||
def get_compose(cfg_data: CfgData) -> Compose:
|
|
||||||
def insert_defaults(data: Compose) -> None:
|
|
||||||
sec_opts = "no-new-privileges:true"
|
|
||||||
data.name = "${_ORG_NAME}"
|
|
||||||
for app_data in data.services.values():
|
|
||||||
app_data.restart = "unless-stopped"
|
|
||||||
if app_data.security_opt is None:
|
|
||||||
app_data.security_opt = {sec_opts}
|
|
||||||
else:
|
|
||||||
app_data.security_opt.add(sec_opts)
|
|
||||||
|
|
||||||
def insert_traefik_labels(data: Compose) -> None:
|
|
||||||
for app_data in data.services.values():
|
|
||||||
traefik_labels = {
|
|
||||||
"traefik.http.routers.${_ORG_NAME}.rule=Host(`${_URL}`)",
|
|
||||||
"traefik.http.routers.${_ORG_NAME}.entrypoints=websecure",
|
|
||||||
"traefik.docker.network=${_ORG_NAME}_proxy",
|
|
||||||
"traefik.http.routers.${_ORG_NAME}.tls.certresolver=le",
|
|
||||||
}
|
|
||||||
if app_data.labels is None:
|
|
||||||
continue
|
|
||||||
if "traefik.enable=true" not in app_data.labels:
|
|
||||||
continue
|
|
||||||
app_data.labels.update(traefik_labels)
|
|
||||||
|
|
||||||
def sub():
|
|
||||||
for path in cfg_data.files:
|
|
||||||
_dict = cast(ComposeServiceYaml, read_yml(path))
|
|
||||||
yield ComposeService.from_dict(_dict)
|
|
||||||
|
|
||||||
compose = reduce(merge_dicts, sub(), cast(Compose, {})) # pyright: ignore[reportInvalidCast]
|
|
||||||
defaults = (insert_defaults, insert_traefik_labels)
|
|
||||||
for func in defaults:
|
|
||||||
func(compose)
|
|
||||||
return compose
|
|
||||||
|
|
||||||
|
|
||||||
def set_networks(data: Compose) -> tuple[str, str] | None:
|
|
||||||
def sub() -> Iterator[Literal["proxy", "internal"]]:
|
|
||||||
for app_data in data["services"].values():
|
|
||||||
if "networks" not in app_data:
|
|
||||||
continue
|
|
||||||
yield from app_data["networks"]
|
|
||||||
|
|
||||||
nets = set(sub())
|
|
||||||
net = ComposeNet()
|
|
||||||
if "proxy" in nets:
|
|
||||||
proxy = ComposeNetArgs(name="${_ORG_NAME}_proxy", external=True)
|
|
||||||
net["proxy"] = proxy
|
|
||||||
ret = "${_ORG_NAME}", "${_ORG_NAME}_proxy"
|
|
||||||
else:
|
|
||||||
ret = None
|
|
||||||
if "internal" in nets:
|
|
||||||
net["internal"] = ComposeNetArgs(name="${_ORG_NAME}")
|
|
||||||
if not net:
|
|
||||||
return
|
|
||||||
data["networks"] = net
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def compose_data_factory(src: str):
|
|
||||||
src_path = src_path_factory(src)
|
|
||||||
cfg_data = config_data_factory(src_path)
|
|
||||||
compose = get_compose(cfg_data)
|
|
||||||
dest_data = dest_data_factory(cfg_data, compose)
|
|
||||||
return ComposeData(
|
|
||||||
src_path,
|
|
||||||
tuple(dest_data),
|
|
||||||
compose,
|
|
||||||
set_networks(compose),
|
|
||||||
to_yaml(compose),
|
|
||||||
)
|
|
||||||
52
src/compose/entity.py
Normal file
52
src/compose/entity.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from typing import Literal, NotRequired, Self, TypedDict, final
|
||||||
|
|
||||||
|
from cfg.entity import CfgData
|
||||||
|
from net.entities import Net, NetTraefik, NetYaml
|
||||||
|
from service.entity import Service, ServiceYaml, TraefikService
|
||||||
|
from service.get import services_get_networks
|
||||||
|
from util import to_yaml
|
||||||
|
|
||||||
|
|
||||||
|
class ComposeYaml(TypedDict):
|
||||||
|
name: str
|
||||||
|
networks: NotRequired[NetYaml]
|
||||||
|
services: dict[str, ServiceYaml]
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Compose:
|
||||||
|
cfg: CfgData
|
||||||
|
networks: Net | None
|
||||||
|
services: dict[str, Service]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, cfg: CfgData, data: ComposeYaml) -> Self:
|
||||||
|
# services = dict[str, ComposeService]()
|
||||||
|
services = dict(get_services_dict(data))
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
cfg,
|
||||||
|
services_get_networks(services.values()),
|
||||||
|
services,
|
||||||
|
)
|
||||||
|
|
||||||
|
def as_yaml(self) -> str:
|
||||||
|
return to_yaml(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
def get_services_dict(data: ComposeYaml):
|
||||||
|
for k, v in data["services"].items():
|
||||||
|
yield k, Service.from_dict(v)
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TraefikCompose:
|
||||||
|
cfg: CfgData
|
||||||
|
networks: NetTraefik
|
||||||
|
services: dict[Literal["traefik"], TraefikService]
|
||||||
|
|
||||||
|
def as_yaml(self) -> str:
|
||||||
|
return to_yaml(asdict(self))
|
||||||
37
src/compose/factory.py
Normal file
37
src/compose/factory.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from cfg import TRAEFIK_PATH
|
||||||
|
from cfg.entity import CfgData
|
||||||
|
from cfg.factory import cfg_data_factory
|
||||||
|
from cfg.get import cfg_get_services
|
||||||
|
from compose.entity import Compose, TraefikCompose
|
||||||
|
from net.entities import NetTraefik
|
||||||
|
from rendered.entity import Rendered
|
||||||
|
from rendered.get import rendered_get_nets
|
||||||
|
from service.entity import TraefikService
|
||||||
|
from service.factory import get_traefik_service
|
||||||
|
from service.get import services_get_networks
|
||||||
|
from src_path.entity import src_paths_factory
|
||||||
|
|
||||||
|
|
||||||
|
def compose_factory(cfg_data: CfgData) -> Compose:
|
||||||
|
services = dict(cfg_get_services(cfg_data))
|
||||||
|
return Compose(
|
||||||
|
cfg_data,
|
||||||
|
services_get_networks(services.values()),
|
||||||
|
services,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def traefik_compose_factory(renders: Iterable[Rendered]) -> TraefikCompose:
|
||||||
|
src_paths = src_paths_factory(TRAEFIK_PATH)
|
||||||
|
cfg = cfg_data_factory(src_paths)
|
||||||
|
service = get_traefik_service()
|
||||||
|
nets: NetTraefik = dict(rendered_get_nets(renders))
|
||||||
|
service["networks"] = list(nets.keys())
|
||||||
|
|
||||||
|
return TraefikCompose(
|
||||||
|
cfg,
|
||||||
|
nets,
|
||||||
|
{"traefik": TraefikService.from_dict(service)},
|
||||||
|
)
|
||||||
26
src/compose/get.py
Normal file
26
src/compose/get.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from compose.entity import Compose, TraefikCompose
|
||||||
|
from util import get_replace_name
|
||||||
|
|
||||||
|
|
||||||
|
def compose_get_volumes(compose: Compose | TraefikCompose) -> Iterator[str]:
|
||||||
|
if isinstance(compose, TraefikCompose):
|
||||||
|
return
|
||||||
|
f = get_replace_name("DATA")
|
||||||
|
for app_data in compose.services.values():
|
||||||
|
if app_data.volumes is None:
|
||||||
|
return
|
||||||
|
for vol in app_data.volumes:
|
||||||
|
if not vol.startswith(f):
|
||||||
|
continue
|
||||||
|
yield vol.split(":", 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
# def compose_get_volumes(compose: Compose | TraefikCompose):
|
||||||
|
|
||||||
|
# return _volumes_sub(compose)
|
||||||
|
# # vols = set(_volumes_sub(compose))
|
||||||
|
# # if not vols:
|
||||||
|
# # return None
|
||||||
|
# # return vols
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from functools import reduce
|
|
||||||
from pathlib import Path
|
|
||||||
from shutil import copyfile
|
|
||||||
|
|
||||||
from .entities import ComposeData, replace
|
|
||||||
|
|
||||||
|
|
||||||
def mk_dir(path: Path):
|
|
||||||
if path.exists():
|
|
||||||
return
|
|
||||||
path.mkdir(parents=True)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_compose_dir(compose_data: ComposeData) -> None:
|
|
||||||
for dest in compose_data.dest_data:
|
|
||||||
mk_dir(dest.paths.data_dir)
|
|
||||||
for vol in dest.volumes:
|
|
||||||
mk_dir(vol)
|
|
||||||
|
|
||||||
|
|
||||||
def mk_compose_env(compose_data: ComposeData) -> None:
|
|
||||||
src = compose_data.src_paths.env_file
|
|
||||||
for dest_data in compose_data.dest_data:
|
|
||||||
dest = dest_data.paths.env_file
|
|
||||||
if src.exists() and not dest.exists():
|
|
||||||
_ = copyfile(src, dest)
|
|
||||||
|
|
||||||
|
|
||||||
def write(compose_data: ComposeData) -> None:
|
|
||||||
for dest_data in compose_data.dest_data:
|
|
||||||
string = reduce(
|
|
||||||
lambda s, rec: replace(rec, s), dest_data.templates, compose_data.yaml
|
|
||||||
)
|
|
||||||
with dest_data.paths.compose_file.open("wt") as f:
|
|
||||||
_ = f.write(string)
|
|
||||||
11
src/dest_path/entity.py
Normal file
11
src/dest_path/entity.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DestPaths:
|
||||||
|
data_dir: Path
|
||||||
|
env_file: Path
|
||||||
|
compose_file: Path
|
||||||
17
src/dest_path/factory.py
Normal file
17
src/dest_path/factory.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from cfg import DATA_ROOT
|
||||||
|
from dest_path.entity import DestPaths
|
||||||
|
from template.entity import Template
|
||||||
|
|
||||||
|
|
||||||
|
def dest_paths_factory(template: Template) -> DestPaths:
|
||||||
|
r_args = template.replace_args
|
||||||
|
path = (
|
||||||
|
DATA_ROOT.joinpath(template.compose.cfg.name)
|
||||||
|
if r_args is None
|
||||||
|
else r_args.data.val.path
|
||||||
|
)
|
||||||
|
return DestPaths(
|
||||||
|
path,
|
||||||
|
path.joinpath(".env"),
|
||||||
|
path.joinpath("docker-compose.yml"),
|
||||||
|
)
|
||||||
46
src/main.py
46
src/main.py
@@ -1,23 +1,33 @@
|
|||||||
from typing import Any, Generator
|
from collections.abc import Iterable, Iterator
|
||||||
|
|
||||||
|
from cfg import CFG_ROOT
|
||||||
|
from cfg.factory import cfg_data_factory
|
||||||
|
from compose.factory import compose_factory, traefik_compose_factory
|
||||||
|
from rendered.entity import Rendered
|
||||||
|
from rendered.factory import rendered_factory
|
||||||
|
from rendered.util import write
|
||||||
|
from src_path.entity import src_paths_factory
|
||||||
|
from template.factory import template_factory
|
||||||
|
|
||||||
|
|
||||||
from collections.abc import Iterator
|
def load_all() -> Iterable[Rendered]:
|
||||||
from Ts import TraefikComposeDict, TraefikNet, TraefikNetName
|
for dir in CFG_ROOT.iterdir():
|
||||||
from compose import gen_compose
|
paths = src_paths_factory(dir)
|
||||||
|
cfg = cfg_data_factory(paths)
|
||||||
|
parsed = compose_factory(cfg)
|
||||||
|
for template in template_factory(parsed):
|
||||||
|
yield rendered_factory(template)
|
||||||
|
|
||||||
|
|
||||||
|
def render_all() -> Iterator[Rendered]:
|
||||||
|
for rendered in load_all():
|
||||||
|
write(rendered)
|
||||||
|
yield rendered
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
paths: tuple[str, ...] = ("gitea", "opencloud", "jellyfin", "immich")
|
renders = render_all()
|
||||||
|
traefik = traefik_compose_factory(renders)
|
||||||
def sub() -> Iterator[tuple[str, str]]:
|
for template in template_factory(traefik):
|
||||||
for path in paths:
|
rendered = rendered_factory(template)
|
||||||
yield from gen_compose(path)
|
write(rendered)
|
||||||
|
|
||||||
# yield name, TraefikNetName(name=proxy)
|
|
||||||
|
|
||||||
networks: TraefikNet = dict(sub())
|
|
||||||
traefik = ComposeBuild("traefik").build()
|
|
||||||
traefik_compose = TraefikComposeDict(
|
|
||||||
name="traefik",
|
|
||||||
networks=networks,
|
|
||||||
)
|
|
||||||
|
|||||||
45
src/net/entities.py
Normal file
45
src/net/entities.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import NotRequired, Self, TypedDict, final
|
||||||
|
|
||||||
|
type NetTraefik = dict[str, NetArgs]
|
||||||
|
|
||||||
|
|
||||||
|
class NetArgsYaml(TypedDict):
|
||||||
|
name: str
|
||||||
|
external: NotRequired[bool]
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class NetArgs:
|
||||||
|
name: str
|
||||||
|
external: bool | None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: NetArgsYaml) -> Self:
|
||||||
|
return cls(
|
||||||
|
data["name"],
|
||||||
|
data.get("external"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NetYaml(TypedDict):
|
||||||
|
internal: NotRequired[NetArgsYaml]
|
||||||
|
proxy: NotRequired[NetArgsYaml]
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Net:
|
||||||
|
internal: NetArgs | None
|
||||||
|
proxy: NetArgs | None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_class(cls, data: NetYaml) -> Self:
|
||||||
|
internal = data.get("internal")
|
||||||
|
if internal is not None:
|
||||||
|
internal = NetArgs.from_dict(internal)
|
||||||
|
proxy = data.get("proxy")
|
||||||
|
if proxy is not None:
|
||||||
|
proxy = NetArgs.from_dict(proxy)
|
||||||
|
return cls(internal, proxy)
|
||||||
21
src/net/factory.py
Normal file
21
src/net/factory.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from net.entities import Net, NetArgs
|
||||||
|
from util import get_replace_name
|
||||||
|
|
||||||
|
|
||||||
|
def net_args_factory(name: str, external: bool | None = None) -> NetArgs:
|
||||||
|
return NetArgs(name, external if external else None)
|
||||||
|
|
||||||
|
|
||||||
|
def net_factory(name: str, _internal: bool, _proxy: bool) -> Net:
|
||||||
|
return Net(
|
||||||
|
net_args_factory(name, _internal) if _internal else None,
|
||||||
|
net_args_factory(f"{name}_proxy", _proxy) if _proxy else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def net_factory_re(_internal: bool, _proxy: bool) -> Net:
|
||||||
|
return net_factory(
|
||||||
|
get_replace_name("name"),
|
||||||
|
_internal,
|
||||||
|
_proxy,
|
||||||
|
)
|
||||||
18
src/rendered/entity.py
Normal file
18
src/rendered/entity.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
from dest_path.entity import DestPaths
|
||||||
|
from src_path.entity import SrcPaths
|
||||||
|
from template.entity import Template
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Rendered:
|
||||||
|
template: Template
|
||||||
|
src_paths: SrcPaths
|
||||||
|
dest_paths: DestPaths
|
||||||
|
volumes: frozenset[Path] | None
|
||||||
|
proxy_net: str | None
|
||||||
|
data: str
|
||||||
25
src/rendered/factory.py
Normal file
25
src/rendered/factory.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
from dest_path.factory import dest_paths_factory
|
||||||
|
from rendered.entity import Rendered
|
||||||
|
from template.entity import Template
|
||||||
|
from template.get import template_get_proxy, template_get_vols
|
||||||
|
|
||||||
|
|
||||||
|
def rendered_factory(template: Template) -> Rendered:
|
||||||
|
yml = template.compose.as_yaml()
|
||||||
|
if template.replace_args is not None:
|
||||||
|
yml = reduce(
|
||||||
|
lambda s, f: f.replace(s),
|
||||||
|
template.replace_args,
|
||||||
|
yml,
|
||||||
|
)
|
||||||
|
vols = frozenset(template_get_vols(template))
|
||||||
|
return Rendered(
|
||||||
|
template,
|
||||||
|
template.compose.cfg.src_paths,
|
||||||
|
dest_paths_factory(template),
|
||||||
|
vols if vols else None,
|
||||||
|
template_get_proxy(template),
|
||||||
|
yml,
|
||||||
|
)
|
||||||
13
src/rendered/get.py
Normal file
13
src/rendered/get.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from collections.abc import Iterable, Iterator
|
||||||
|
|
||||||
|
from net.entities import NetArgs
|
||||||
|
from net.factory import net_args_factory
|
||||||
|
from rendered.entity import Rendered
|
||||||
|
|
||||||
|
|
||||||
|
def rendered_get_nets(renders: Iterable[Rendered]) -> Iterator[tuple[str, NetArgs]]:
|
||||||
|
for render in renders:
|
||||||
|
net = render.proxy_net
|
||||||
|
if net is None:
|
||||||
|
continue
|
||||||
|
yield net, net_args_factory(f"{net}_proxy")
|
||||||
38
src/rendered/util.py
Normal file
38
src/rendered/util.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
from rendered.entity import Rendered
|
||||||
|
|
||||||
|
|
||||||
|
def _mk_dir(path: Path) -> None:
|
||||||
|
if path.exists():
|
||||||
|
return
|
||||||
|
path.mkdir(parents=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _mk_compose_dir(rendered: Rendered) -> None:
|
||||||
|
_mk_dir(rendered.dest_paths.data_dir)
|
||||||
|
vols = rendered.volumes
|
||||||
|
if vols is None:
|
||||||
|
return
|
||||||
|
for vol in vols:
|
||||||
|
_mk_dir(vol)
|
||||||
|
|
||||||
|
|
||||||
|
def _mk_compose_env(rendered: Rendered) -> None:
|
||||||
|
src = rendered.src_paths.env_file
|
||||||
|
dest = rendered.dest_paths.env_file
|
||||||
|
if src.exists() and not dest.exists():
|
||||||
|
_ = copyfile(src, dest)
|
||||||
|
|
||||||
|
|
||||||
|
def write_raw(path: Path, data: str) -> None:
|
||||||
|
with path.open("wt") as f:
|
||||||
|
_ = f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
def write(rendered: Rendered) -> None:
|
||||||
|
funcs = (_mk_compose_dir, _mk_compose_env)
|
||||||
|
for func in funcs:
|
||||||
|
func(rendered)
|
||||||
|
write_raw(rendered.dest_paths.compose_file, rendered.data)
|
||||||
158
src/service/entity.py
Normal file
158
src/service/entity.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal, NotRequired, Self, TypedDict, TypeVar, overload, override
|
||||||
|
|
||||||
|
from cfg import T_Primitive
|
||||||
|
from util import get_replace_name
|
||||||
|
|
||||||
|
type T_NetAbc = str | Literal["proxy", "internal"]
|
||||||
|
TCo_NetABC = TypeVar("TCo_NetABC", bound=T_NetAbc, covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceYamlAbc[T_net: T_NetAbc](TypedDict):
|
||||||
|
command: NotRequired[list[str]]
|
||||||
|
container_name: str
|
||||||
|
entrypoint: list[str]
|
||||||
|
environment: NotRequired[dict[str, T_Primitive]]
|
||||||
|
image: str
|
||||||
|
labels: NotRequired[list[str]]
|
||||||
|
logging: dict[str, str]
|
||||||
|
networks: NotRequired[list[T_net]]
|
||||||
|
restart: str
|
||||||
|
security_opt: NotRequired[list[str]]
|
||||||
|
user: NotRequired[str]
|
||||||
|
volumes: NotRequired[list[str]]
|
||||||
|
|
||||||
|
|
||||||
|
TCo_ServiceYaml = TypeVar(
|
||||||
|
"TCo_ServiceYaml",
|
||||||
|
bound=ServiceYamlAbc[T_NetAbc],
|
||||||
|
covariant=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceYaml(ServiceYamlAbc[Literal["proxy", "internal"]]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikServiceYaml(ServiceYamlAbc[str]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
type T_Compose = ServiceYaml | TraefikServiceYaml
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ServiceAbc[T_net: T_NetAbc, T_Yaml: T_Compose](ABC):
|
||||||
|
command: tuple[str, ...] | None
|
||||||
|
container_name: str
|
||||||
|
entrypoint: tuple[str, ...]
|
||||||
|
environment: dict[str, T_Primitive] | None
|
||||||
|
image: str
|
||||||
|
labels: frozenset[str] | None
|
||||||
|
logging: dict[str, str] | None
|
||||||
|
networks: frozenset[T_net] | None
|
||||||
|
restart: str
|
||||||
|
security_opt: frozenset[str]
|
||||||
|
user: str | None
|
||||||
|
volumes: frozenset[str] | None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: T_Yaml) -> Self:
|
||||||
|
command = data.get("command")
|
||||||
|
volumes = data.get("volumes")
|
||||||
|
return cls(
|
||||||
|
tuple(command) if command else None,
|
||||||
|
data["container_name"],
|
||||||
|
tuple(data["entrypoint"]),
|
||||||
|
data.get("environment"),
|
||||||
|
data["image"],
|
||||||
|
_get_labels(data),
|
||||||
|
None,
|
||||||
|
cls.get_nets(data),
|
||||||
|
"unless-stopped",
|
||||||
|
_get_sec_opts(data),
|
||||||
|
data.get("user"),
|
||||||
|
frozenset(volumes) if volumes else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_valid(self) -> bool:
|
||||||
|
attrs = (self.container_name, self.restart, self.logging)
|
||||||
|
for attr in attrs:
|
||||||
|
if attr is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
@staticmethod
|
||||||
|
def get_nets(data: T_Yaml) -> frozenset[T_net] | None:
|
||||||
|
pass
|
||||||
|
# return self._get_nets(data)
|
||||||
|
|
||||||
|
|
||||||
|
class Service(ServiceAbc[Literal["proxy", "internal"], ServiceYaml]):
|
||||||
|
@override
|
||||||
|
@staticmethod
|
||||||
|
def get_nets(data: ServiceYaml) -> frozenset[Literal["proxy", "internal"]] | None:
|
||||||
|
return _get_nets(data)
|
||||||
|
|
||||||
|
|
||||||
|
class TraefikService(ServiceAbc[str, TraefikServiceYaml]):
|
||||||
|
@override
|
||||||
|
@staticmethod
|
||||||
|
def get_nets(data: TraefikServiceYaml) -> frozenset[str] | None:
|
||||||
|
return _get_nets(data)
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def _get_nets(
|
||||||
|
data: ServiceYaml,
|
||||||
|
) -> frozenset[Literal["proxy", "internal"]] | None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def _get_nets(data: TraefikServiceYaml) -> frozenset[str] | None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _get_nets(
|
||||||
|
data: ServiceYaml | TraefikServiceYaml,
|
||||||
|
) -> frozenset[str] | frozenset[Literal["proxy", "internal"]] | None:
|
||||||
|
nets = data.get("networks")
|
||||||
|
if nets is None:
|
||||||
|
return
|
||||||
|
return frozenset(nets)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sec_opts(
|
||||||
|
data: T_Compose,
|
||||||
|
) -> frozenset[str]:
|
||||||
|
sec_opts = frozenset(
|
||||||
|
"no-new-privileges:true",
|
||||||
|
)
|
||||||
|
sec = data.get("security_opt")
|
||||||
|
if not sec:
|
||||||
|
return sec_opts
|
||||||
|
return sec_opts.union(sec)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_labels(
|
||||||
|
data: T_Compose,
|
||||||
|
) -> frozenset[str] | None:
|
||||||
|
org_name = get_replace_name("org_name")
|
||||||
|
url = get_replace_name("url")
|
||||||
|
traefik_labels = frozenset(
|
||||||
|
(
|
||||||
|
f"traefik.http.routers.{org_name}.rule=Host(`{url}`)",
|
||||||
|
f"traefik.http.routers.{org_name}.entrypoints=websecure",
|
||||||
|
f"traefik.docker.network={org_name}_proxy",
|
||||||
|
f"traefik.http.routers.{org_name}.tls.certresolver=le",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
labels = data.get("labels")
|
||||||
|
if not labels:
|
||||||
|
return
|
||||||
|
if "traefik.enable=true" not in labels:
|
||||||
|
return frozenset(labels)
|
||||||
|
return traefik_labels.union(labels)
|
||||||
10
src/service/factory.py
Normal file
10
src/service/factory.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
from cfg import TRAEFIK_PATH
|
||||||
|
from service.entity import TraefikServiceYaml
|
||||||
|
from util import read_yml
|
||||||
|
|
||||||
|
|
||||||
|
def get_traefik_service():
|
||||||
|
path = TRAEFIK_PATH.joinpath("service.yml")
|
||||||
|
return cast(TraefikServiceYaml, read_yml(path))
|
||||||
26
src/service/get.py
Normal file
26
src/service/get.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from collections.abc import Iterable, Iterator
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from net.entities import Net
|
||||||
|
from net.factory import net_factory_re
|
||||||
|
from service.entity import Service
|
||||||
|
|
||||||
|
|
||||||
|
def _networks_sub(
|
||||||
|
services: Iterable[Service],
|
||||||
|
) -> Iterator[Literal["proxy", "internal"]]:
|
||||||
|
for app_data in services:
|
||||||
|
networks = app_data.networks
|
||||||
|
if networks is None:
|
||||||
|
continue
|
||||||
|
yield from networks
|
||||||
|
|
||||||
|
|
||||||
|
def services_get_networks(services: Iterable[Service]) -> Net | None:
|
||||||
|
networks = frozenset(_networks_sub(services))
|
||||||
|
if not networks:
|
||||||
|
return None
|
||||||
|
return net_factory_re(
|
||||||
|
"internal" in networks,
|
||||||
|
"proxy" in networks,
|
||||||
|
)
|
||||||
19
src/src_path/entity.py
Normal file
19
src/src_path/entity.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class SrcPaths:
|
||||||
|
cfg_dir: Path
|
||||||
|
cfg_file: Path
|
||||||
|
env_file: Path
|
||||||
|
|
||||||
|
|
||||||
|
def src_paths_factory(src: Path) -> SrcPaths:
|
||||||
|
return SrcPaths(
|
||||||
|
cfg_dir=src,
|
||||||
|
cfg_file=src.joinpath("cfg.yml"),
|
||||||
|
env_file=src.joinpath(".env"),
|
||||||
|
)
|
||||||
7
src/src_path/get.py
Normal file
7
src/src_path/get.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from cfg.entity import CfgDataYaml
|
||||||
|
from src_path.entity import SrcPaths
|
||||||
|
|
||||||
|
|
||||||
|
def src_path_get_files(src_paths: SrcPaths, data: CfgDataYaml):
|
||||||
|
for path in data["files"]:
|
||||||
|
yield src_paths.cfg_dir.joinpath(path)
|
||||||
32
src/template/entity.py
Normal file
32
src/template/entity.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
from compose.entity import Compose, TraefikCompose
|
||||||
|
from template.val_obj import DataDir, NameVal, OrgVal, RecordCls, Url
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ReplaceArgs:
|
||||||
|
org: RecordCls[OrgVal]
|
||||||
|
name: RecordCls[NameVal]
|
||||||
|
org_name: RecordCls[NameVal]
|
||||||
|
data: RecordCls[DataDir]
|
||||||
|
url: RecordCls[Url]
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield self.org
|
||||||
|
yield self.name
|
||||||
|
yield self.org_name
|
||||||
|
yield self.data
|
||||||
|
yield self.url
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Template:
|
||||||
|
compose: Compose | TraefikCompose
|
||||||
|
replace_args: ReplaceArgs | None
|
||||||
|
# dest_paths: DestPaths
|
||||||
|
volumes: frozenset[str] | None
|
||||||
|
# proxy_net: str | None
|
||||||
49
src/template/factory.py
Normal file
49
src/template/factory.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
from cfg import DATA_ROOT
|
||||||
|
from cfg.entity import CfgData, OrgData
|
||||||
|
from compose.entity import Compose, TraefikCompose
|
||||||
|
from compose.get import compose_get_volumes
|
||||||
|
from template.entity import ReplaceArgs, Template
|
||||||
|
from template.val_obj import DataDir, NameVal, OrgVal, Record, Url
|
||||||
|
|
||||||
|
|
||||||
|
def replace_args_factory(cfg_data: CfgData, org_data: OrgData) -> ReplaceArgs:
|
||||||
|
_org = OrgVal(org_data.org)
|
||||||
|
_name = NameVal(cfg_data.name)
|
||||||
|
org_name = (
|
||||||
|
NameVal(f"{_org.to_str()}_{_name.to_str()}") if _org.is_valid() else _name
|
||||||
|
)
|
||||||
|
data_dir = DATA_ROOT.joinpath(_org.to_str(), _name.to_str())
|
||||||
|
|
||||||
|
return ReplaceArgs(
|
||||||
|
Record("org", _org),
|
||||||
|
Record("name", _name),
|
||||||
|
Record("org_name", org_name),
|
||||||
|
Record("data", DataDir(data_dir)),
|
||||||
|
Record("url", Url(org_data.url)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def template_factory(compose: Compose | TraefikCompose) -> Iterator[Template]:
|
||||||
|
cfg_data = compose.cfg
|
||||||
|
vols = frozenset(compose_get_volumes(compose))
|
||||||
|
if not vols:
|
||||||
|
vols = None
|
||||||
|
|
||||||
|
orgs = cfg_data.orgs
|
||||||
|
if orgs is None:
|
||||||
|
yield Template(compose, None, vols)
|
||||||
|
return
|
||||||
|
|
||||||
|
for org_data in orgs:
|
||||||
|
args = replace_args_factory(
|
||||||
|
cfg_data,
|
||||||
|
org_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield Template(
|
||||||
|
compose,
|
||||||
|
args,
|
||||||
|
vols,
|
||||||
|
)
|
||||||
36
src/template/get.py
Normal file
36
src/template/get.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from collections.abc import Iterable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from net.entities import Net
|
||||||
|
from template.entity import Template
|
||||||
|
|
||||||
|
|
||||||
|
def template_get_vols(template: Template) -> Iterable[Path]:
|
||||||
|
def _lambda(x: str) -> str:
|
||||||
|
return x
|
||||||
|
|
||||||
|
vols = template.volumes
|
||||||
|
if not vols:
|
||||||
|
return
|
||||||
|
r_args = template.replace_args
|
||||||
|
re = _lambda if r_args is None else r_args.data.replace
|
||||||
|
for vol in vols:
|
||||||
|
yield Path(re(vol))
|
||||||
|
|
||||||
|
|
||||||
|
def template_get_proxy(template: Template) -> str | None:
|
||||||
|
proxy = template.compose.networks
|
||||||
|
if proxy is None:
|
||||||
|
return
|
||||||
|
if not isinstance(proxy, Net):
|
||||||
|
return
|
||||||
|
if proxy.proxy is None:
|
||||||
|
return
|
||||||
|
if proxy.internal is None:
|
||||||
|
net = proxy.proxy.name.replace("_proxy", "")
|
||||||
|
else:
|
||||||
|
net = proxy.internal.name
|
||||||
|
r_args = template.replace_args
|
||||||
|
if r_args is None:
|
||||||
|
return net
|
||||||
|
return r_args.name.replace(net)
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Protocol, final
|
from typing import TypeVar, final, override
|
||||||
|
|
||||||
|
from util import get_replace_name
|
||||||
|
|
||||||
|
|
||||||
class RecordVal(Protocol):
|
class RecordVal(ABC):
|
||||||
def to_str(self) -> str: ...
|
@abstractmethod
|
||||||
|
def to_str(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
TCo_RecordVal = TypeVar(
|
||||||
|
"TCo_RecordVal",
|
||||||
|
bound=RecordVal,
|
||||||
|
covariant=True,
|
||||||
|
)
|
||||||
|
TCon_RecordVal = TypeVar(
|
||||||
|
"TCon_RecordVal",
|
||||||
|
bound=RecordVal,
|
||||||
|
contravariant=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@@ -13,12 +30,21 @@ class RecordCls[T: RecordVal]:
|
|||||||
name: str
|
name: str
|
||||||
val: T
|
val: T
|
||||||
|
|
||||||
|
# @final
|
||||||
|
# class RecordClsProto(Protocol):
|
||||||
|
# name:str
|
||||||
|
# val: RecordVal
|
||||||
|
|
||||||
|
def replace(self, string: str) -> str:
|
||||||
|
return str.replace(string, self.name, self.val.to_str())
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class OrgVal:
|
class OrgVal(RecordVal):
|
||||||
val: str | None
|
val: str | None
|
||||||
|
|
||||||
|
@override
|
||||||
def to_str(self) -> str:
|
def to_str(self) -> str:
|
||||||
if self.val is None:
|
if self.val is None:
|
||||||
return "personal"
|
return "personal"
|
||||||
@@ -30,36 +56,35 @@ class OrgVal:
|
|||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class NameVal:
|
class NameVal(RecordVal):
|
||||||
val: str
|
val: str
|
||||||
|
|
||||||
|
@override
|
||||||
def to_str(self) -> str:
|
def to_str(self) -> str:
|
||||||
return self.val
|
return self.val
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class DataDir:
|
class DataDir(RecordVal):
|
||||||
path: Path
|
path: Path
|
||||||
|
|
||||||
|
@override
|
||||||
def to_str(self) -> str:
|
def to_str(self) -> str:
|
||||||
return str(self.path)
|
return str(self.path)
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class Url:
|
class Url(RecordVal):
|
||||||
sub_url: str | None
|
sub_url: str | None
|
||||||
|
|
||||||
|
@override
|
||||||
def to_str(self) -> str:
|
def to_str(self) -> str:
|
||||||
if self.sub_url is None:
|
if self.sub_url is None:
|
||||||
return ""
|
return ""
|
||||||
return ".".join([self.sub_url, "ccamper7", "net"])
|
return ".".join([self.sub_url, "ccamper7", "net"])
|
||||||
|
|
||||||
|
|
||||||
# def get_replace_var(string: str) -> str:
|
|
||||||
# return f"${{_{name}}}"
|
|
||||||
|
|
||||||
|
|
||||||
def Record[T: RecordVal](name: str, val: T) -> RecordCls[T]:
|
def Record[T: RecordVal](name: str, val: T) -> RecordCls[T]:
|
||||||
return RecordCls(f"${{_{name}}}", val)
|
return RecordCls(get_replace_name(name), val)
|
||||||
18
src/util.py
18
src/util.py
@@ -5,7 +5,7 @@ from typing import Any, cast, override
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from compose.compose_struct import T_PrimDict, T_PrimVal, T_Primitive
|
from cfg import T_PrimDict, T_Primitive, T_PrimVal, T_YamlDict
|
||||||
|
|
||||||
|
|
||||||
class VerboseSafeDumper(yaml.SafeDumper):
|
class VerboseSafeDumper(yaml.SafeDumper):
|
||||||
@@ -16,8 +16,8 @@ class VerboseSafeDumper(yaml.SafeDumper):
|
|||||||
|
|
||||||
def merge_dicts[T: Mapping[Any, Any]](dict1: T, dict2: T) -> T:
|
def merge_dicts[T: Mapping[Any, Any]](dict1: T, dict2: T) -> T:
|
||||||
def _merge_dicts(dict1: T_PrimDict, dict2: T_PrimDict):
|
def _merge_dicts(dict1: T_PrimDict, dict2: T_PrimDict):
|
||||||
s1 = set(dict1.keys())
|
s1 = frozenset(dict1.keys())
|
||||||
s2 = set(dict2.keys())
|
s2 = frozenset(dict2.keys())
|
||||||
for k in s1.difference(s2):
|
for k in s1.difference(s2):
|
||||||
yield k, dict1[k]
|
yield k, dict1[k]
|
||||||
for k in s2.difference(s1):
|
for k in s2.difference(s1):
|
||||||
@@ -29,18 +29,22 @@ def merge_dicts[T: Mapping[Any, Any]](dict1: T, dict2: T) -> T:
|
|||||||
yield k, dict[T_Primitive, T_PrimVal](_merge_dicts(v1, v2))
|
yield k, dict[T_Primitive, T_PrimVal](_merge_dicts(v1, v2))
|
||||||
continue
|
continue
|
||||||
if isinstance(v1, list) and isinstance(v2, list):
|
if isinstance(v1, list) and isinstance(v2, list):
|
||||||
yield k, list(set(v1).union(v2))
|
yield k, list(frozenset(v1).union(v2))
|
||||||
continue
|
continue
|
||||||
raise Exception("merge error")
|
raise Exception("merge error")
|
||||||
|
|
||||||
return cast(T, dict(_merge_dicts(dict1, dict2)))
|
return cast(T, dict(_merge_dicts(dict1, dict2)))
|
||||||
|
|
||||||
|
|
||||||
def read_yml(path: Path) -> Mapping[Any, Any]: # pyright: ignore[reportExplicitAny]
|
def read_yml(path: Path) -> T_YamlDict:
|
||||||
with path.open("rt") as f:
|
with path.open("rt") as f:
|
||||||
return yaml.safe_load(f) # pyright: ignore[reportAny]
|
return cast(T_YamlDict, yaml.safe_load(f))
|
||||||
|
|
||||||
|
|
||||||
def to_yaml(data: Mapping[Any, Any]) -> str: # pyright: ignore[reportExplicitAny]
|
def to_yaml(data: T_YamlDict) -> str:
|
||||||
_yaml = yaml.dump(data, Dumper=VerboseSafeDumper)
|
_yaml = yaml.dump(data, Dumper=VerboseSafeDumper)
|
||||||
return re.sub(r"(^\s*-)", r" \g<1>", _yaml, flags=re.MULTILINE)
|
return re.sub(r"(^\s*-)", r" \g<1>", _yaml, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def get_replace_name(name: str) -> str:
|
||||||
|
return f"${{_{name.upper()}}}"
|
||||||
|
|||||||
110
uv.lock
generated
Normal file
110
uv.lock
generated
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "basedpyright"
|
||||||
|
version = "1.36.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ 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" }
|
||||||
|
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" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compose"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "basedpyright" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "basedpyright", specifier = ">=1.36.1" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0.3" },
|
||||||
|
{ name = "ruff", specifier = ">=0.14.9" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodejs-wheel-binaries"
|
||||||
|
version = "24.12.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b9/35/d806c2ca66072e36dc340ccdbeb2af7e4f1b5bcc33f1481f00ceed476708/nodejs_wheel_binaries-24.12.0.tar.gz", hash = "sha256:f1b50aa25375e264697dec04b232474906b997c2630c8f499f4caf3692938435", size = 8058, upload-time = "2025-12-11T21:12:26.856Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/3b/9d6f044319cd5b1e98f07c41e2465b58cadc1c9c04a74c891578f3be6cb5/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:7564ddea0a87eff34e9b3ef71764cc2a476a8f09a5cccfddc4691148b0a47338", size = 55125859, upload-time = "2025-12-11T21:11:58.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/a5/f5722bf15c014e2f476d7c76bce3d55c341d19122d8a5d86454db32a61a4/nodejs_wheel_binaries-24.12.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:8ff929c4669e64613ceb07f5bbd758d528c3563820c75d5de3249eb452c0c0ab", size = 55309035, upload-time = "2025-12-11T21:12:01.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/61/68d39a6f1b5df67805969fd2829ba7e80696c9af19537856ec912050a2be/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:6ebacefa8891bc456ad3655e6bce0af7e20ba08662f79d9109986faeb703fd6f", size = 59661017, upload-time = "2025-12-11T21:12:05.268Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/a1/31aad16f55a5e44ca7ea62d1367fc69f4b6e1dba67f58a0a41d0ed854540/nodejs_wheel_binaries-24.12.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3292649a03682ccbfa47f7b04d3e4240e8c46ef04dc941b708f20e4e6a764f75", size = 60159770, upload-time = "2025-12-11T21:12:08.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/5e/b7c569aa1862690ca4d4daf3a64cafa1ea6ce667a9e3ae3918c56e127d9b/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7fb83df312955ea355ba7f8cbd7055c477249a131d3cb43b60e4aeb8f8c730b1", size = 61653561, upload-time = "2025-12-11T21:12:12.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/87/567f58d7ba69ff0208be849b37be0f2c2e99c69e49334edd45ff44f00043/nodejs_wheel_binaries-24.12.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2473c819448fedd7b036dde236b09f3c8bbf39fbbd0c1068790a0498800f498b", size = 62238331, upload-time = "2025-12-11T21:12:16.143Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/9d/c6492188ce8de90093c6755a4a63bb6b2b4efb17094cb4f9a9a49c73ed3b/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_amd64.whl", hash = "sha256:2090d59f75a68079fabc9b86b14df8238b9aecb9577966dc142ce2a23a32e9bb", size = 41342076, upload-time = "2025-12-11T21:12:20.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/af/cd3290a647df567645353feed451ef4feaf5844496ced69c4dcb84295ff4/nodejs_wheel_binaries-24.12.0-py2.py3-none-win_arm64.whl", hash = "sha256:d0c2273b667dd7e3f55e369c0085957b702144b1b04bfceb7ce2411e58333757", size = 39048104, upload-time = "2025-12-11T21:12:23.495Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.14.9"
|
||||||
|
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" }
|
||||||
|
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user