diff --git a/pillar_tool/git/__init__.py b/pillar_tool/git/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pillar_tool/git/repository.py b/pillar_tool/git/repository.py new file mode 100644 index 0000000..bde0b11 --- /dev/null +++ b/pillar_tool/git/repository.py @@ -0,0 +1,97 @@ +import os + +import pygit2 +from pygit2 import RemoteCallbacks, CredentialType, Username, UserPass, Keypair +from pygit2.callbacks import _Credentials +from pygit2.enums import BranchType, FetchPrune, CheckoutStrategy as CS + +from pillar_tool.util import Config + + +class RepoCallbacks(RemoteCallbacks): + def __init__(self, config: Config): + super().__init__() + self.config = config + + def credentials( + self, + url: str, + username_from_url: str | None, + allowed_types: CredentialType, + ) -> Username | UserPass | Keypair: + # compute allowed methods + allowed = [ 2**i for i,v in enumerate(reversed(bin(15)[2:])) if int(v) ] + + if CredentialType.SSH_KEY.value in allowed: + print("cred ssh_key") + return Keypair( + username=self.config.git.state_repo_user, + privkey=self.config.git.state_repo_keyfile, + pubkey=self.config.git.state_repo_pubkeyfile, + passphrase=None + ) + elif CredentialType.USERNAME.value in allowed: + print("cred username") + return Username(self.config.git.state_repo_user) + + print(f"The remote requested invalid credentials: {allowed}") + raise RuntimeError(f"The remote requested invalid credentials: {allowed}") + + + + +def checkout_remote_branch(config: Config, branch_name: str) -> None: + """Checkout a remote branch from the state repository. + + Verifies that the state repository exists and has an 'origin' remote, + then checks out the specified remote branch. + + Args: + config: Configuration object containing the git state repository path. + branch_name: Name of the remote branch to checkout. + + Raises: + FileNotFoundError: If the state repository directory does not exist. + ValueError: If the repository cannot be opened, 'origin' remote is missing, + or the specified branch does not exist. + """ + # create an instance of the RepositoryCallback class + cbs = RepoCallbacks(config) + + # check if the repository actually exists + if not os.path.isdir(config.git.state_repo_path): + # if the directory does not yet exist, clone the repository + try: + print("cloning state repo") + os.makedirs(os.path.dirname(config.git.state_repo_path), mode=0o700, exist_ok=True) + repository = pygit2.clone_repository(config.git.state_repo_remote, config.git.state_repo_path, callbacks=cbs, depth=1) + except Exception as e: + print(f"Failed to clone state repo: {e}") + raise ValueError(f"Unable to clone the states repository: {e}") + else: + # directory exists, so attempt to open the repository + try: + repository = pygit2.Repository(config.git.state_repo_path) + except Exception: + raise ValueError(f"State repo at {config.git.state_repo_path} cannot be opened") + + # check whether this repository has a remote named origin + # this only needs to happen when the repo has not just been cloned + if "origin" not in repository.remotes: + raise ValueError(f"No remote named origin in repo at {config.git.state_repo_path}") + else: + repository.remotes["origin"].fetch(prune=FetchPrune.PRUNE, depth=1, callbacks=cbs) + + # check if the requested branch exists + try: + branch_ref = repository.lookup_branch(f'origin/{branch_name}', BranchType.REMOTE) + except KeyError: + raise ValueError(f"Branch '{branch_name}' does not exist in the repository.") + + try: + # checkout the remote branch with force + # this should be done like this, since there should never be any change made in this clone of the repository + repository.checkout(branch_ref, callbacks=cbs, strategy=CS.FORCE | CS.RECREATE_MISSING | CS.REMOVE_UNTRACKED) + except Exception as exc: + raise ValueError(f"Failed to checkout branch: {exc}") + diff --git a/pillar_tool/main.py b/pillar_tool/main.py index 6a8dd78..d8cdd27 100644 --- a/pillar_tool/main.py +++ b/pillar_tool/main.py @@ -28,6 +28,7 @@ from pillar_tool.routers.hostgroup import router as hostgroup_router from pillar_tool.routers.environment import router as environment_router from pillar_tool.routers.state import router as state_router from pillar_tool.routers.pillar import router as pillar_router +from pillar_tool.routers.top import router as top_router # run any pending migrations run_db_migrations() @@ -76,6 +77,7 @@ app.include_router(hostgroup_router) app.include_router(environment_router) app.include_router(state_router) app.include_router(pillar_router) +app.include_router(top_router) @app.get("/") async def root(): diff --git a/pillar_tool/ptcli/cli/__init__.py b/pillar_tool/ptcli/cli/__init__.py index f6f1c70..674f2c5 100644 --- a/pillar_tool/ptcli/cli/__init__.py +++ b/pillar_tool/ptcli/cli/__init__.py @@ -3,4 +3,5 @@ from .hostgroup import hostgroup from .query import query from .state import state from .pillar import pillar -from .environment import environment \ No newline at end of file +from .environment import environment +from .top import top \ No newline at end of file diff --git a/pillar_tool/ptcli/cli/environment.py b/pillar_tool/ptcli/cli/environment.py index b3f0c97..20069ab 100644 --- a/pillar_tool/ptcli/cli/environment.py +++ b/pillar_tool/ptcli/cli/environment.py @@ -2,7 +2,7 @@ import click import requests from .cli_main import main, auth_header, base_url -from pillar_tool.util.validation import split_and_validate_path +from pillar_tool.util.validation import split_and_validate_path, validate_environment_name @main.group("environment") @@ -54,7 +54,6 @@ def environment_create(name: str): """Create a new environment.""" click.echo(f"Creating environment '{name}'...") try: - from pillar_tool.util.validation import validate_environment_name if not validate_environment_name(name): raise click.ClickException( "Invalid environment name. Use only alphanumeric, underscore or dash characters.") @@ -74,7 +73,6 @@ def environment_delete(name: str): """Delete an environment by name.""" click.echo(f"Deleting environment '{name}'...") try: - from pillar_tool.util.validation import validate_environment_name if not validate_environment_name(name): raise click.ClickException( "Invalid environment name. Use only alphanumeric, underscore or dash characters.") @@ -93,4 +91,26 @@ def environment_delete(name: str): f"Assigned hosts: {', '.join(hosts_list) if hosts_list else 'none'}" ) else: - raise click.ClickException(f"Failed to delete environment:\n{e}") \ No newline at end of file + raise click.ClickException(f"Failed to delete environment:\n{e}") + +@environment.command("import") +@click.argument("name") +def environment_import(name: str): + """Import an environment by name.""" + click.echo(f"Importing environment '{name}'...") + + try: + if not validate_environment_name(name): + raise click.ClickException( + "Invalid environment name. Use only alphanumeric, underscore or dash characters.") + + response = requests.patch(f'{base_url()}/environment/{name}', headers=auth_header()) + response.raise_for_status() + + click.echo(f"Environment '{name}' imported successfully.") + except requests.exceptions.HTTPError as e: + if e.response is not None: + raise click.ClickException(f"Failed to import environment:\n{e}") + else: + raise click.ClickException(f"Failed to import environment:\n{e}") + diff --git a/pillar_tool/ptcli/cli/top.py b/pillar_tool/ptcli/cli/top.py new file mode 100644 index 0000000..ba6d6a1 --- /dev/null +++ b/pillar_tool/ptcli/cli/top.py @@ -0,0 +1,38 @@ +import click +import requests + +from .cli_main import main, auth_header, base_url + + +@main.group("top") +def top(): + pass + + +@top.command("get") +@click.argument("host") +def top_get(host: str): + click.echo("Querying top for host...") + try: + response = requests.get(f'{base_url()}/top/query/{host}', headers=auth_header()) + response.raise_for_status() + + click.echo("Top:") + click.echo(response.json()) + except requests.exceptions.HTTPError as e: + raise click.ClickException(f"Failed to query top:\n{e}") + + +@top.command("setenv") +@click.argument("host") +@click.argument("environment") +def top_setenv(host: str, environment: str): + click.echo("Assigning environment to host...") + + try: + response = requests.post(f'{base_url()}/top/setenv/{host}/{environment}', headers=auth_header()) + response.raise_for_status() + + click.echo("Assigned environment") + except requests.exceptions.HTTPError as e: + raise click.ClickException(f"Failed to assign environment:\n{e}") \ No newline at end of file diff --git a/pillar_tool/routers/environment.py b/pillar_tool/routers/environment.py index 0c89258..7ac75b6 100644 --- a/pillar_tool/routers/environment.py +++ b/pillar_tool/routers/environment.py @@ -1,17 +1,20 @@ +import os import uuid +import pygit2 +from pygit2.enums import BranchType - -from sqlalchemy import select, insert, bindparam, delete +from sqlalchemy import select, insert, delete from sqlalchemy.orm import Session from starlette.exceptions import HTTPException from starlette.requests import Request -from fastapi import APIRouter, Query, Depends +from fastapi import APIRouter from starlette.responses import JSONResponse from pillar_tool.db import Host from pillar_tool.db.models.top_data import Environment, EnvironmentAssignment -from pillar_tool.schemas import HostgroupParams, get_model_from_query -from pillar_tool.util.validation import split_and_validate_path, validate_environment_name +from pillar_tool.util.validation import validate_environment_name +from pillar_tool.util import config, Config +from pillar_tool.git.repository import checkout_remote_branch router = APIRouter( prefix="/environment", @@ -177,3 +180,14 @@ def environment_delete(req: Request, name: str): return JSONResponse(status_code=204, content={}) +@router.patch("/{name}") +def environment_patch(req: Request, name: str) -> JSONResponse: + + db: Session = req.state.db + cfg: Config = config() + + # Attempt to check the requested branch out + try: + checkout_remote_branch(cfg, name) + except Exception as exc: + raise HTTPException(status_code=404, detail=str(exc)) \ No newline at end of file diff --git a/pillar_tool/routers/top.py b/pillar_tool/routers/top.py new file mode 100644 index 0000000..b494c19 --- /dev/null +++ b/pillar_tool/routers/top.py @@ -0,0 +1,95 @@ +import uuid + +from sqlalchemy import select, insert, delete, and_, bindparam +from sqlalchemy.orm import Session +from starlette.exceptions import HTTPException +from starlette.requests import Request +from fastapi import APIRouter +from starlette.responses import JSONResponse + +from pillar_tool.db import Host, Environment, EnvironmentAssignment +from pillar_tool.db.models.top_data import State, StateAssignment +from pillar_tool.util.validation import validate_state_name + +router = APIRouter( + prefix="/top", + tags=["top"], +) + + +@router.get("/query/{host}") +def top_get(req: Request, host: str): + db: Session = req.state.db + + # build the hierarchy + host_stmt = select(Host).where(Host.name == host) + result = db.execute(host_stmt).fetchall() + if len(result) == 0: + return JSONResponse(status_code=404, content={"message": "Host '{}' not found".format(host)}) + elif len(result) > 1: + return JSONResponse(status_code=500, content={"message": "More than one host found"}) + else: + target_host = result[0][0] + + parent_stmt = select(Host).where(Host.id == bindparam("parent_id")) + parents = [] + current = target_host + while current is not None: + result = db.execute(parent_stmt, {'parend_id': current.id}).fetchall() + if len(result) == 0: + current = None + elif len(result) > 1: + return JSONResponse(status_code=500, content={"message": "More than one parent host found"}) + else: + parents.append(result[0][0]) + current = result[0][0] + + + + # TODO: states should be hierarchical, same as pillars are + select_stmt = (select(Host, EnvironmentAssignment, Environment) + .where(and_(Host.name == host, Host.is_hostgroup == False)) + .join(EnvironmentAssignment, EnvironmentAssignment.host_id == Host.id) + .join(Environment, EnvironmentAssignment.environment_id == Environment.id) + ) + + result = db.execute(select_stmt).fetchall() + print(result[0]) + + return JSONResponse(status_code=200, content={}) + + +@router.post("/setenv/{host}/{environment}") +def top_setenv(req: Request, host: str, environment: str): + db: Session = req.state.db + + # get the target host id + host_stmt = select(Host).where(and_(Host.name == host, Host.is_hostgroup == False)) + host_res = db.execute(host_stmt).fetchall() + if len(host_res) == 0: + return JSONResponse(status_code=404, content={"error": "No host found"}) + elif len(host_res) == 1: + host_res = host_res[0][0] + else: + # Note that this should be prevented by the database + return JSONResponse(status_code=404, content={"error": "Too many hosts found??? This should not happen"}) + + # get the environment id + env_stmt = select(Environment).where(Environment.name == environment) + env_res = db.execute(env_stmt).fetchall() + if len(env_res) == 0: + return JSONResponse(status_code=404, content={"error": "No environment found"}) + elif len(env_res) == 1: + env_res = env_res[0][0] + else: + # Note that this should be prevented by the database + return JSONResponse(status_code=404, content={"error": "Too many environments found??? This should not happen"}) + + insert_stmt = insert(EnvironmentAssignment).values(environment_id=env_res.id, host_id=host_res.id) + result = db.execute(insert_stmt) + + return JSONResponse(status_code=200, content={}) + + +def top_state_assign(req: Request): + pass diff --git a/pillar_tool/util/config.py b/pillar_tool/util/config.py index 5e9197e..b73a7ab 100644 --- a/pillar_tool/util/config.py +++ b/pillar_tool/util/config.py @@ -14,6 +14,13 @@ class RuntimeConfig(BaseModel): host: str port: int +class GitConfig(BaseModel): + state_repo_path: str + state_repo_remote: str + state_repo_user: str + state_repo_keyfile: str + state_repo_pubkeyfile: str + class PTCLIConfig(BaseModel): scheme: str host: str @@ -24,4 +31,5 @@ class PTCLIConfig(BaseModel): class Config(BaseModel): db: DatabaseConfig runtime: RuntimeConfig - ptcli: PTCLIConfig \ No newline at end of file + ptcli: PTCLIConfig + git: GitConfig \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a420480..00a2693 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "psycopg2>=2.9.11", "pycryptodome>=3.23.0", "pydantic>=2.12.5", + "pygit2>=1.19.2", "pyyaml>=6.0.3", "requests>=2.32.5", "sqlalchemy>=2.0.45", diff --git a/uv.lock b/uv.lock index e933b92..76080b1 100644 --- a/uv.lock +++ b/uv.lock @@ -55,6 +55,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -316,6 +361,7 @@ dependencies = [ { name = "psycopg2" }, { name = "pycryptodome" }, { name = "pydantic" }, + { name = "pygit2" }, { name = "pyyaml" }, { name = "requests" }, { name = "sqlalchemy" }, @@ -331,6 +377,7 @@ requires-dist = [ { name = "psycopg2", specifier = ">=2.9.11" }, { name = "pycryptodome", specifier = ">=3.23.0" }, { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pygit2", specifier = ">=1.19.2" }, { name = "pyyaml", specifier = ">=6.0.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "sqlalchemy", specifier = ">=2.0.45" }, @@ -355,6 +402,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/08/737aa39c78d705a7ce58248d00eeba0e9fc36be488f9b672b88736fbb1f7/psycopg2-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:f10a48acba5fe6e312b891f290b4d2ca595fc9a06850fe53320beac353575578", size = 2803738, upload-time = "2025-10-10T11:10:23.196Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pycryptodome" version = "3.23.0" @@ -453,6 +509,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, ] +[[package]] +name = "pygit2" +version = "1.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/a4/10ce00feef5c43eddacab19ae6610c4d4ef3ab77e544e9ee938772cd1c17/pygit2-1.19.2.tar.gz", hash = "sha256:cbeb3dbca9ca6ee3d5ea5d02f5e844c2d6084a2d5d6621e3e06aa2b11c645bfd", size = 803448, upload-time = "2026-03-29T14:57:27.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/77/c925eee8496961729f029a4edda67485c7637248c0e730e0b41122357be5/pygit2-1.19.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:df207f93a33851a110dec70108e3f2a1c69578932919fd356303eda83a5624db", size = 5704802, upload-time = "2026-03-29T14:56:31.635Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fc/d46428b7ea0ce7bd3cac73b73206a2cba50580f54b58bd704d8755d5658c/pygit2-1.19.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae884cd53e29b3d831f5261f36048a8d5db5642dc98cd63530810e7fd9c9e60d", size = 5696329, upload-time = "2026-03-29T14:56:33.343Z" }, + { url = "https://files.pythonhosted.org/packages/35/05/a3bb39095ef31e140cbeb30abbd08fafb13ed70b656a9de095fac74a1ff5/pygit2-1.19.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bd4059964531d20aaf4577b3761590df9cc7c9e2395df5d33f0552224331b76", size = 6036095, upload-time = "2026-03-29T14:56:34.836Z" }, + { url = "https://files.pythonhosted.org/packages/4c/cb/36ebd241351bd1ced1f126bf0b21fbb6c0d48ce36122512cc51cde83d10b/pygit2-1.19.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3befcccc7b3b62e45da2cc1ce4095964f7606d3d15b43dc667c6ef2a2ada20d", size = 4637435, upload-time = "2026-03-29T14:56:36.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/35/779d6b8e9df0cc3236f675af5fc37e4047e1a6ab96f9c72ef5b5ed8d888b/pygit2-1.19.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cf08b54553f997f6f60a7918504e22e7baa4ba2fbb11d1e1cb6c0a45ac7e04b", size = 5799881, upload-time = "2026-03-29T14:56:38.04Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/cb361f4bd5342fa01a0f83b04eff8873a09771183bcb6e29947078577119/pygit2-1.19.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7f630e5a763f01b4be6e2374c487086229c8f7392a2e5591d29095c5e481da4", size = 6042342, upload-time = "2026-03-29T14:56:39.523Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/b9ea61266eb7d568ea17d8fec63dc766ebecec23860b4e5ac5bcfbbe15d7/pygit2-1.19.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6166845f41d4f6be3353997022d64035fe3df348c8e34d7d30c5f95817fbcab4", size = 5770452, upload-time = "2026-03-29T14:56:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/403532429072a61d5498d17ddf6be3258953e73b6499f70a2b4e1345bb84/pygit2-1.19.2-cp313-cp313-win32.whl", hash = "sha256:5bebea045102e87dea142242298d4dd668d0227f76042f98efb1c5d5dd3db21e", size = 946658, upload-time = "2026-03-29T14:56:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/01/08/6f37fb23514da02345889d7be7cea899d2a348fa4871492ea9a8837e70e4/pygit2-1.19.2-cp313-cp313-win_amd64.whl", hash = "sha256:7bbfeb680821001a5c1b6959da1eae906806c90c9992ae4564d3ea83a27bb19f", size = 1164264, upload-time = "2026-03-29T14:56:43.753Z" }, + { url = "https://files.pythonhosted.org/packages/90/b9/d11220d5f0cfc92895b02814ab36ac94edbf46ae1b9dc3077c457d03d718/pygit2-1.19.2-cp313-cp313-win_arm64.whl", hash = "sha256:033d489186145cf67b2c60840d2a308f6b1e9d641de12417c447f9829dacde70", size = 969348, upload-time = "2026-03-29T14:56:44.892Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/1a935baeb29958d7e50a52c7a963ce5963f24fa8a5024e1082d43b07a770/pygit2-1.19.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5effee3f4ad0d9c89b34ebecf1acee26f6b117ef3c51345ad022bd521fd8dca", size = 5706909, upload-time = "2026-03-29T14:56:46.249Z" }, + { url = "https://files.pythonhosted.org/packages/a7/86/4bb6f196b13bd7ed825f4e931fb7152a36d01e8de24c8de44425702ad18c/pygit2-1.19.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ed09804dc6b6de0be07a71443122fd7b6458f8466d1134003c2dea55af886fc", size = 5696293, upload-time = "2026-03-29T14:56:48.173Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/d674b3f854cecf53bccbc21a095734759cd3599624578ed3c78602eb22a3/pygit2-1.19.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d114aa066e718d5ef3401b366dcb0b37b549c3b3b139f5f0042bd7059a4b0f7", size = 6038057, upload-time = "2026-03-29T14:56:50.118Z" }, + { url = "https://files.pythonhosted.org/packages/64/eb/2ce41735e27ee0f28f786aae62ea371f3beec0ef38d1712a2910421386c4/pygit2-1.19.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1becc06071acfdd5ae8523aaeab6d4b0930b2bcb08f5eb878e052e61275000b", size = 4641475, upload-time = "2026-03-29T14:56:51.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8d/35f6096c42caefb715ca29e991279b493275c0051a3c83081099644d3f4a/pygit2-1.19.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06d2db3bdbf2906eb17112adb14a2fe6e34c1b2bce39c91819f59208d4e56665", size = 5801738, upload-time = "2026-03-29T14:56:53.043Z" }, + { url = "https://files.pythonhosted.org/packages/fc/31/dbbaa7a433008fec9046cc293c012ae5d5a31e66321e1fb05d64ae131e54/pygit2-1.19.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8a7e99e5dfc8d3ed8f849b9688bc3fb1bdc86f34af28159140a8d1e18b703dd8", size = 6043074, upload-time = "2026-03-29T14:56:54.774Z" }, + { url = "https://files.pythonhosted.org/packages/f0/33/b34266efba6917081dafb50976155c2d31cd377f277e67348a810245c4b4/pygit2-1.19.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7659d59eba6c4a706978237d02e8d719f960843df749256f1656c938c1f4142b", size = 5770986, upload-time = "2026-03-29T14:56:56.796Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ab/813f3af50987020cd90e810da147ebef16a61003b9af995070ec338634ba/pygit2-1.19.2-cp314-cp314-win32.whl", hash = "sha256:e551908dfd93d471c0b08cfcddbe4924417865aae6ac90d20f3815c9483b0a82", size = 967943, upload-time = "2026-03-29T14:56:58.196Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/24df5ac51a316e36a07bbf9e4c91fade523b9e80a84d5c9e7acd10b22248/pygit2-1.19.2-cp314-cp314-win_amd64.whl", hash = "sha256:eb1fd8538372230f8a471a5f3629901bc2fc7df992853d97bedc8fa269a9caf3", size = 1194774, upload-time = "2026-03-29T14:56:59.721Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ee/274a91b28864fd9c5cdd2949b4d7e0909fd6a89785a46308de098d3a22cd/pygit2-1.19.2-cp314-cp314-win_arm64.whl", hash = "sha256:3cc461245b70be45a936e925744e67a45f6b0ee970aeb8e7a385dd7fe9f40877", size = 996677, upload-time = "2026-03-29T14:57:01.013Z" }, + { url = "https://files.pythonhosted.org/packages/4f/22/3c05a56918e6fda5deb53aeb7436959a8880f4cc436a76771771479693de/pygit2-1.19.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cb686bc81dfe5b13937047643fddb1dd253dae33b4a9ca62858c49ed294e05be", size = 5710172, upload-time = "2026-03-29T14:57:02.672Z" }, + { url = "https://files.pythonhosted.org/packages/59/eb/2fdd485c01b478c77dd2e949b424a61c70a8750ffb13c5035fe3edf6a8f6/pygit2-1.19.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ec3538d81963bd05dd16c0de75938a9173966e1c853ad7848ebcb60bcfe21b0", size = 5699256, upload-time = "2026-03-29T14:57:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/51/f4/f0608bb369da15f2973dfb33e7b7cba4c9bc8164e6a01e3f15e65e85efef/pygit2-1.19.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d02ebb50ea082d9631bbfda12787eb5324b8880a72cb8e3b9f11e9b323ad5781", size = 6096321, upload-time = "2026-03-29T14:57:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1b/816d3700dc8bcc9028c5f81b190f2d770d1cb9cd2ccdd39939d0b6730718/pygit2-1.19.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a3643e4dd569c2909e88586659f617f70315680ca3c619cd8ff9e9c28726c25", size = 4696179, upload-time = "2026-03-29T14:57:08.552Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a6/0fc82f07c4dfee5856626c5d4b422c32e14cac0204eb1e9558ac0d717b07/pygit2-1.19.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:697e3684cb4ef2bfc084623c3f680d5ae8b4c8afca31a35a731b7b70204d9f83", size = 5853368, upload-time = "2026-03-29T14:57:10.449Z" }, + { url = "https://files.pythonhosted.org/packages/f8/60/0393786d7810b7f83def3738cb9be1a735cf6b555dc219d90f46010b87b1/pygit2-1.19.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:173165b54a2affed918302193f12dd369bec981b1d77904cdcd76b966a824e15", size = 6099319, upload-time = "2026-03-29T14:57:12.166Z" }, + { url = "https://files.pythonhosted.org/packages/13/ad/22e30e630a147e10a912e085c4cb816a0dc39bee8d39493b40101f3da4c7/pygit2-1.19.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ff32adce1a48d76b10e790b36784f6cb5ef40699b758c8b84f7f53f13b13d237", size = 5822074, upload-time = "2026-03-29T14:57:13.84Z" }, + { url = "https://files.pythonhosted.org/packages/4b/08/71ea683386887a1aab8f9b8c282b6df7ce7fae45fc7c9959719c78baebba/pygit2-1.19.2-cp314-cp314t-win32.whl", hash = "sha256:637d7c023f6623da35cf02cd1091f260c709730dd615367f4524ec8d771d0898", size = 972866, upload-time = "2026-03-29T14:57:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/da/67/efbde3954bdcbadfb61d183badd9a3e730c4ad94ed10966abe0b177abe0c/pygit2-1.19.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2805a8abd546e38298ce5daf33e444960e483acce68cbfb5d338e72ad5bc3503", size = 1201537, upload-time = "2026-03-29T14:57:16.72Z" }, + { url = "https://files.pythonhosted.org/packages/0f/0c/28ae2c74038d1c51092f525658986a261f1963ec96528e7b41e721387343/pygit2-1.19.2-cp314-cp314t-win_arm64.whl", hash = "sha256:376a0d2c27c082f6bd8b97fd8ffc1939f16dfe8374ec846deee9b11151b37b8a", size = 997795, upload-time = "2026-03-29T14:57:17.878Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3"