From 1d615e9f7da68b3c6d08ed4ce59f56edde5500e9 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Tue, 7 Oct 2025 00:18:37 +0200 Subject: [PATCH 1/3] VCS settings --- app/config/settings.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/config/settings.py b/app/config/settings.py index cf95017..a20f301 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -1,18 +1,25 @@ from functools import lru_cache - -from pydantic_settings import BaseSettings, SettingsConfigDict -from pydantic import BaseModel from pathlib import Path + import yaml +from pydantic import BaseModel +from pydantic_settings import BaseSettings, SettingsConfigDict + class AppConfig(BaseModel): host: str = "127.0.0.1" port: int = 8000 reload: bool = True +class GitConfig(BaseModel): + directory: str = "/opt/repo/sample" + branch: str = "master" + remote: str = "origin" + class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="KARL_", env_nested_delimiter="__") app: AppConfig = AppConfig() + git: GitConfig = GitConfig() @classmethod def from_yaml(cls, path: Path | str = "config/config.yaml") -> "Settings": @@ -23,6 +30,7 @@ class Settings(BaseSettings): data = yaml.safe_load(fh) or {} return cls(**data) + @lru_cache def get_settings() -> Settings: return Settings.from_yaml() From a55628ce35a7484161676ac971d33a7c4a22ca37 Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Tue, 7 Oct 2025 00:34:39 +0200 Subject: [PATCH 2/3] VCS service --- app/services/vcs.py | 28 ++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 29 insertions(+) diff --git a/app/services/vcs.py b/app/services/vcs.py index e69de29..c137108 100644 --- a/app/services/vcs.py +++ b/app/services/vcs.py @@ -0,0 +1,28 @@ +from git import Repo, Remote + +from app.config import get_settings + + +class GitService: + def __init__(self): + self._settings = get_settings() + self._repo = Repo(self._settings.git.directory) + self._origin: Remote = self._repo.remotes.origin + + def get_modified_compose(self) -> str | None: + self._update() + return self._diff() + + def _update(self): + self._origin.pull() + + def _diff(self) -> str | None: + diff = self._repo.head.commit.diff("HEAD~1") + composes = [f for f in diff if f.a_path.endswith("docker-compose.yml")] + match len(composes): + case 0: + return None + case 1: + return composes[0].a_path + case _: + raise Exception("Multiple compose files modified") diff --git a/pyproject.toml b/pyproject.toml index c7db9ab..1b8f359 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "jinja2>=3.1.4", "pydantic-settings>=2.4.0", "pyyaml>=6.0.2", + "gitpython>=3.1.45" ] [project.optional-dependencies] From 162c0adf13ad1633b4454eb19fe587aa2c1eb03c Mon Sep 17 00:00:00 2001 From: Piotr Dec Date: Wed, 8 Oct 2025 00:05:13 +0200 Subject: [PATCH 3/3] Keepass outline --- app/config/settings.py | 15 ++++-- app/{services/passwd.py => model/__init__.py} | 0 app/model/passwords.py | 49 +++++++++++++++++++ app/services/passwords.py | 38 ++++++++++++++ app/services/system.py | 1 + app/util/__init__.py | 0 app/util/dicts.py | 11 +++++ config/config.yaml | 3 ++ pyproject.toml | 1 + 9 files changed, 115 insertions(+), 3 deletions(-) rename app/{services/passwd.py => model/__init__.py} (100%) create mode 100644 app/model/passwords.py create mode 100644 app/services/passwords.py create mode 100644 app/services/system.py create mode 100644 app/util/__init__.py create mode 100644 app/util/dicts.py diff --git a/app/config/settings.py b/app/config/settings.py index cf95017..33a7019 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -1,18 +1,26 @@ from functools import lru_cache - -from pydantic_settings import BaseSettings, SettingsConfigDict -from pydantic import BaseModel from pathlib import Path + import yaml +from pydantic import BaseModel +from pydantic_settings import BaseSettings, SettingsConfigDict + class AppConfig(BaseModel): host: str = "127.0.0.1" port: int = 8000 reload: bool = True + +class KeePassConfig(BaseModel): + file: str = "database.kdbx" + secret: Path | str = "/run/secrets/kp_secret" + + class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="KARL_", env_nested_delimiter="__") app: AppConfig = AppConfig() + kp: KeePassConfig = KeePassConfig() @classmethod def from_yaml(cls, path: Path | str = "config/config.yaml") -> "Settings": @@ -23,6 +31,7 @@ class Settings(BaseSettings): data = yaml.safe_load(fh) or {} return cls(**data) + @lru_cache def get_settings() -> Settings: return Settings.from_yaml() diff --git a/app/services/passwd.py b/app/model/__init__.py similarity index 100% rename from app/services/passwd.py rename to app/model/__init__.py diff --git a/app/model/passwords.py b/app/model/passwords.py new file mode 100644 index 0000000..d795d2c --- /dev/null +++ b/app/model/passwords.py @@ -0,0 +1,49 @@ +from dataclasses import dataclass, field +from typing import Type + +# TODO: unnecessary? + +@dataclass +class PathItem: + name: str + t: Type + +@dataclass +class Path: + path: list[PathItem] = field(default_factory=list) + + def append(self, name, t): + self.path.append(PathItem(name, t)) + + def __str__(self): + return "/".join([i.name for i in self.path]) + + +@dataclass +class Group: + name: str + passwords: list["Password"] = field(default_factory=list) + parent: "Group|None" = None + + @property + def path(self): + if self.parent is None: + new_path = Path() + new_path.append(self.name, type(self)) + return new_path + return self.parent.path.append(self.name, type(self)) + + +@dataclass +class Password: + name: str + group: Group + + @property + def path(self): + return self.group.path.append(self.name, type(self)) + +class UnencryptedPassword(Password): + def __init__(self, name: str, value: str, group: Group): + super().__init__(name, group) + self.value = value diff --git a/app/services/passwords.py b/app/services/passwords.py new file mode 100644 index 0000000..9dca423 --- /dev/null +++ b/app/services/passwords.py @@ -0,0 +1,38 @@ +import os.path + +from pykeepass import PyKeePass, create_database, Group + + +class Passwords: + def __init__(self): + from app.config import get_settings + settings = get_settings() + + with open(settings.kp.secret, "r") as fh: + secret = fh.read() + + self._kp_org = self.__get_or_create_store(settings.kp.file, secret) + self._kp = self.__get_lock(settings.kp.file, secret) + + @staticmethod + def __get_or_create_store(path, passwd) -> PyKeePass: + if os.path.exists(path): + return PyKeePass( + path, + password=passwd, + ) + return create_database(path, passwd) + + @staticmethod + def __get_lock(path, passwd) -> PyKeePass: + lock_path = path + ".lock" + import shutil + shutil.copyfile(path, lock_path) + return Passwords.__get_or_create_store(lock_path, passwd) + + @property + def store(self): + return self._kp.root_group + + def save(self, group: Group): + pass diff --git a/app/services/system.py b/app/services/system.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/services/system.py @@ -0,0 +1 @@ + diff --git a/app/util/__init__.py b/app/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/util/dicts.py b/app/util/dicts.py new file mode 100644 index 0000000..faf7f65 --- /dev/null +++ b/app/util/dicts.py @@ -0,0 +1,11 @@ +from types import SimpleNamespace + + +class NestedNamespace(SimpleNamespace): + def __init__(self, dictionary, **kwargs): + super().__init__(**kwargs) + for key, value in dictionary.items(): + if isinstance(value, dict): + self.__setattr__(key, NestedNamespace(value)) + else: + self.__setattr__(key, value) diff --git a/config/config.yaml b/config/config.yaml index 8afdd9a..5b47197 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -2,3 +2,6 @@ app: host: "127.0.0.1" port: 8000 reload: true +kp: + file: "config/kp.kdbx" + secret: "config/secret.txt" diff --git a/pyproject.toml b/pyproject.toml index c7db9ab..0f8ebcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "jinja2>=3.1.4", "pydantic-settings>=2.4.0", "pyyaml>=6.0.2", + "pykeepass>=4.1.1.post1" ] [project.optional-dependencies]