diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d799ad1f..ed247280 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,6 +228,10 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: "Set environment variables" + run: | + RUNNER_NAME=$(echo "${{ runner.name }}" | grep -o 'ghrunner[0-9]\+' | sed 's/ghrunner\([0-9]\+\)/ghrunner_\1/') + echo "PYTEST_DEBUG_TEMPROOT=/var/lib/github/${RUNNER_NAME}/_temp" >> $GITHUB_ENV - name: "Setup environment" run: | pipx install poetry==2.1 --python python${{ matrix.python-version }} diff --git a/docs/docs/infrahubctl/infrahubctl-repository.mdx b/docs/docs/infrahubctl/infrahubctl-repository.mdx index 94375125..2addb7a9 100644 --- a/docs/docs/infrahubctl/infrahubctl-repository.mdx +++ b/docs/docs/infrahubctl/infrahubctl-repository.mdx @@ -19,6 +19,7 @@ $ infrahubctl repository [OPTIONS] COMMAND [ARGS]... **Commands**: * `add`: Add a new repository. +* `init`: Initialize a new Infrahub repository. * `list` ## `infrahubctl repository add` @@ -47,6 +48,26 @@ $ infrahubctl repository add [OPTIONS] NAME LOCATION * `--config-file TEXT`: [env var: INFRAHUBCTL_CONFIG; default: infrahubctl.toml] * `--help`: Show this message and exit. +## `infrahubctl repository init` + +Initialize a new Infrahub repository. + +**Usage**: + +```console +$ infrahubctl repository init [OPTIONS] DST +``` + +**Arguments**: + +* `DST`: [required] + +**Options**: + +* `--data PATH` +* `--config-file TEXT`: [env var: INFRAHUBCTL_CONFIG; default: infrahubctl.toml] +* `--help`: Show this message and exit. + ## `infrahubctl repository list` **Usage**: diff --git a/infrahub_sdk/ctl/example_repo/.infrahub.yml b/infrahub_sdk/ctl/example_repo/.infrahub.yml new file mode 100644 index 00000000..27d38000 --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/.infrahub.yml @@ -0,0 +1,3 @@ +--- +schemas: + - schemas diff --git a/infrahub_sdk/ctl/example_repo/README.md b/infrahub_sdk/ctl/example_repo/README.md new file mode 100644 index 00000000..fc1d6799 --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/README.md @@ -0,0 +1,36 @@ +# Infrahub Repository + +Welcome! This repository was initialized via the `infrahubctl repo init` command. That bootstraps a repository for use with some example data. + +## Installation + +Running `poetry install` will install all the main dependencies you need to interact with this repository. + +## Starting Infrahub + +Included in the repository are a set of helper commands to get Infrahub up and running using `invoke`. + +```bash +Available tasks: + + destroy Stop and remove containers, networks, and volumes. + download-compose-file Download docker-compose.yml from InfraHub if missing or override is True. + load-schema Load schemas into InfraHub using infrahubctl. + restart Restart all services or a specific one using docker-compose. + start Start the services using docker-compose in detached mode. + stop Stop containers and remove networks. + test Run tests using pytest. +``` + +To start infrahub simply use `invoke start` + +## Tests + +By default there are some integration tests that will spin up Infrahub and its dependencies in docker and load the repository and schema. This can be run using the following: + +```bash +poetry install --with=dev +pytest tests/integration +``` + +To change the version of infrahub being used you can use an environment variable: `export INFRAHUB_TESTING_IMAGE_VERSION=1.2.5`. \ No newline at end of file diff --git a/infrahub_sdk/ctl/example_repo/copier.yml b/infrahub_sdk/ctl/example_repo/copier.yml new file mode 100644 index 00000000..57b5613b --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/copier.yml @@ -0,0 +1,32 @@ +--- +project_name: + type: str + help: What is your project name? +generators: + type: bool + help: Would you like to use generators? + default: false +transforms: + type: bool + help: Would you like to use transforms? + default: false +scripts: + type: bool + help: Would you like to have local scripts? + default: false +queries: + type: bool + help: Would you like to store queries? + default: false +menus: + type: bool + help: Would you like to store menus? + default: false +tests: + type: bool + help: Would you like to have testing? + default: false +package_mode: + type: bool + help: Would you like to have a python library initialized? + default: false diff --git a/infrahub_sdk/ctl/example_repo/pyproject.toml.jinja b/infrahub_sdk/ctl/example_repo/pyproject.toml.jinja new file mode 100644 index 00000000..e1c758dc --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/pyproject.toml.jinja @@ -0,0 +1,28 @@ +[tool.poetry] +name = "{{ project_name }}" +version = "0.1.0" +description = "Infrahub Repository" +authors = [] +readme = "README.md" +{% if package_mode %} +packages = [ + { include = "lib" } +] +{% else %} +package-mode = false +{% endif %} + +[tool.poetry.dependencies] +python = ">=3.9,<=3.13.1" +infrahub-sdk = { extras = ["all"], version = "*" } +invoke = "*" + +[tool.poetry.group.dev.dependencies] +ruff = "*" +pytest = "*" +infrahub-testcontainers = "*" +pytest-asyncio = "*" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/infrahub_sdk/ctl/example_repo/schemas/example.yml b/infrahub_sdk/ctl/example_repo/schemas/example.yml new file mode 100644 index 00000000..3d1ab684 --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/schemas/example.yml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://schema.infrahub.app/infrahub/schema/latest.json +--- +version: "1.0" +nodes: + - name: Device + namespace: Network + human_friendly_id: ['hostname__value'] + attributes: + - name: hostname + kind: Text + unique: true + - name: model + kind: Text + relationships: + - name: interfaces + cardinality: many + peer: NetworkInterface + kind: Component + - name: Interface + namespace: Network + attributes: + - name: name + kind: Text + - name: description + kind: Text + optional: true + relationships: + - name: device + cardinality: one + peer: NetworkDevice + optional: false + kind: Parent diff --git a/infrahub_sdk/ctl/example_repo/tasks.py b/infrahub_sdk/ctl/example_repo/tasks.py new file mode 100644 index 00000000..0d36f620 --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/tasks.py @@ -0,0 +1,83 @@ +import os +from pathlib import Path + +import httpx +from invoke import Context, task + +# If no version is indicated, we will take the latest +VERSION = os.getenv("INFRAHUB_IMAGE_VER", None) + + +@task +def start(context: Context) -> None: + """ + Start the services using docker-compose in detached mode. + """ + download_compose_file(context, override=False) + context.run("docker compose up -d") + + +@task +def destroy(context: Context) -> None: + """ + Stop and remove containers, networks, and volumes. + """ + download_compose_file(context, override=False) + context.run("docker compose down -v") + + +@task +def stop(context: Context) -> None: + """ + Stop containers and remove networks. + """ + download_compose_file(context, override=False) + context.run("docker compose down") + + +@task(help={"component": "Optional name of a specific service to restart."}) +def restart(context: Context, component: str = "") -> None: + """ + Restart all services or a specific one using docker-compose. + """ + download_compose_file(context, override=False) + if component: + context.run(f"docker compose restart {component}") + return + + context.run("docker compose restart") + + +@task +def load_schema(ctx: Context) -> None: + """ + Load schemas into InfraHub using infrahubctl. + """ + ctx.run("infrahubctl schema load schemas") + + +@task +def test(ctx: Context) -> None: + """ + Run tests using pytest. + """ + ctx.run("pytest tests") + + +@task(help={"override": "Redownload the compose file even if it already exists."}) +def download_compose_file(context: Context, override: bool = False) -> Path: # noqa ARG001 + """ + Download docker-compose.yml from InfraHub if missing or override is True. + """ + compose_file = Path("./docker-compose.yml") + + if compose_file.exists() and not override: + return compose_file + + response = httpx.get("https://infrahub.opsmill.io") + response.raise_for_status() + + with compose_file.open("w") as f: + f.write(response.content.decode()) + + return compose_file diff --git a/infrahub_sdk/ctl/example_repo/{% if package_mode %}lib{% endif %}/__init__.py b/infrahub_sdk/ctl/example_repo/{% if package_mode %}lib{% endif %}/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infrahub_sdk/ctl/example_repo/{% if package_mode %}lib{% endif %}/example.py b/infrahub_sdk/ctl/example_repo/{% if package_mode %}lib{% endif %}/example.py new file mode 100644 index 00000000..cfb4b3ac --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/{% if package_mode %}lib{% endif %}/example.py @@ -0,0 +1,8 @@ +import logging + +from infrahub_sdk.node import InfrahubNode + + +def print_nodes(log: logging.Logger, nodes: list[InfrahubNode]): + for node in nodes.keys(): + log.info(f"{node} present.") diff --git a/infrahub_sdk/ctl/example_repo/{% if scripts %}scripts{% endif %}/example_script.py b/infrahub_sdk/ctl/example_repo/{% if scripts %}scripts{% endif %}/example_script.py new file mode 100644 index 00000000..84bf6907 --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/{% if scripts %}scripts{% endif %}/example_script.py @@ -0,0 +1,15 @@ +import logging + +from lib.example import print_nodes + +from infrahub_sdk import InfrahubClient + + +async def run( + client: InfrahubClient, + log: logging.Logger, + branch: str, +): + log.info(f"Running example script on {branch}...") + nodes = await client.schema.all() + print_nodes(log, nodes) diff --git a/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/__init__.py b/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/integration/conftest.py b/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/integration/conftest.py new file mode 100644 index 00000000..3bd0a177 --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/integration/conftest.py @@ -0,0 +1,27 @@ +from pathlib import Path +from typing import Any + +import pytest + +from infrahub_sdk.yaml import SchemaFile + +CURRENT_DIRECTORY = Path(__file__).parent.resolve() + + +@pytest.fixture +def root_directory() -> Path: + """ + Return the path of the root directory of the repository. + """ + return CURRENT_DIRECTORY.parent.parent + + +@pytest.fixture +def schemas_directory(root_directory: Path) -> Path: + return root_directory / "schemas" + + +@pytest.fixture +def schemas(schemas_directory: Path) -> list[dict[str, Any]]: + schema_files = SchemaFile.load_from_disk(paths=[schemas_directory]) + return [item.content for item in schema_files if item.content] diff --git a/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/integration/test_infrahub.py b/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/integration/test_infrahub.py new file mode 100644 index 00000000..dd3f4626 --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/{% if tests %}tests{% endif %}/integration/test_infrahub.py @@ -0,0 +1,42 @@ +from pathlib import Path + +import pytest + +from infrahub_sdk import InfrahubClient +from infrahub_sdk.protocols import CoreGenericRepository +from infrahub_sdk.testing.docker import TestInfrahubDockerClient +from infrahub_sdk.testing.repository import GitRepo + + +class TestInfrahub(TestInfrahubDockerClient): + @pytest.mark.asyncio + async def test_load_schema(self, default_branch: str, client: InfrahubClient, schemas: list[dict]): + await client.schema.wait_until_converged(branch=default_branch) + + resp = await client.schema.load(schemas=schemas, branch=default_branch, wait_until_converged=True) + assert resp.errors == {} + + @pytest.mark.asyncio + async def test_load_repository( + self, + client: InfrahubClient, + remote_repos_dir: Path, + root_directory: Path, + ) -> None: + """Add the local directory as a repository in Infrahub and wait for the import to be complete""" + + repo = GitRepo( + name="local-repository", + src_directory=root_directory, + dst_directory=remote_repos_dir, + ) + await repo.add_to_infrahub(client=client) + in_sync = await repo.wait_for_sync_to_complete(client=client) + assert in_sync + + repos = await client.all(kind=CoreGenericRepository) + + # A breakpoint can be added to pause the tests from running and keep the test containers active + # breakpoint() + + assert repos diff --git a/infrahub_sdk/ctl/example_repo/{{_copier_conf.answers_file}}.jinja b/infrahub_sdk/ctl/example_repo/{{_copier_conf.answers_file}}.jinja new file mode 100644 index 00000000..ea97bd4b --- /dev/null +++ b/infrahub_sdk/ctl/example_repo/{{_copier_conf.answers_file}}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier +{{ _copier_answers|to_nice_yaml -}} \ No newline at end of file diff --git a/infrahub_sdk/ctl/repository.py b/infrahub_sdk/ctl/repository.py index 1992a537..08de9610 100644 --- a/infrahub_sdk/ctl/repository.py +++ b/infrahub_sdk/ctl/repository.py @@ -1,10 +1,12 @@ from __future__ import annotations +import asyncio from pathlib import Path from typing import Optional import typer import yaml +from copier import run_copy from pydantic import ValidationError from rich.console import Console from rich.table import Table @@ -165,3 +167,24 @@ async def list( ) console.print(table) + + +@app.command() +async def init( + dst: Path, + data: Optional[Path] = None, + _: str = CONFIG_PARAM, +) -> None: + """Initialize a new Infrahub repository.""" + example_repo = Path(__file__).parent / "example_repo" + config_data = None + if data: + try: + with Path.open(data, encoding="utf-8") as f: + config_data = yaml.safe_load(f) # Load YAML contents + typer.echo(f"Loaded config: {config_data}") # Print for debugging + except Exception as e: + typer.echo(f"Error loading YAML file: {e}", err=True) + raise typer.Exit(code=1) + + await asyncio.to_thread(run_copy, str(example_repo), str(dst), data=config_data) diff --git a/poetry.lock b/poetry.lock index ac79c770..464ea1ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -220,7 +220,36 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "(extra == \"ctl\" or extra == \"all\") and platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {main = "(extra == \"ctl\" or extra == \"all\") and platform_system == \"Windows\" or sys_platform == \"win32\" or extra == \"ctl\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "copier" +version = "9.6.0" +description = "A library for rendering project templates." +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"ctl\"" +files = [ + {file = "copier-9.6.0-py3-none-any.whl", hash = "sha256:aaf992600a373fa2dda9f61725916a230c210ede14a968c60dab855d524c6cf3"}, + {file = "copier-9.6.0.tar.gz", hash = "sha256:e05a18b387b96e8d1fbd5271d37f59c9a02be5ad717f7878d0505562fd62b786"}, +] + +[package.dependencies] +colorama = ">=0.4.6" +dunamai = ">=1.7.0" +eval-type-backport = {version = ">=0.1.3,<0.3.0", markers = "python_version < \"3.10\""} +funcy = ">=1.17" +jinja2 = ">=3.1.5" +jinja2-ansible-filters = ">=1.3.1" +packaging = ">=23.0" +pathspec = ">=0.9.0" +platformdirs = ">=4.3.6" +plumbum = ">=1.6.9" +pydantic = ">=2.4.2" +pygments = ">=2.7.1" +pyyaml = ">=5.3.1" +questionary = ">=1.8.1" [[package]] name = "coverage" @@ -446,6 +475,22 @@ https = ["urllib3 (>=1.24.1)"] paramiko = ["paramiko"] pgp = ["gpg"] +[[package]] +name = "dunamai" +version = "1.23.1" +description = "Dynamic version generation" +optional = true +python-versions = ">=3.5" +groups = ["main"] +markers = "extra == \"ctl\"" +files = [ + {file = "dunamai-1.23.1-py3-none-any.whl", hash = "sha256:2611b0b9105a5797149ef82f4968a01dd912bdac857d49fc06856a4cfa58cf78"}, + {file = "dunamai-1.23.1.tar.gz", hash = "sha256:0b5712fc63bfb235263d912bfc5eb84590ba2201bb737268d25a5dbad7085489"}, +] + +[package.dependencies] +packaging = ">=20.9" + [[package]] name = "eval-type-backport" version = "0.2.2" @@ -525,6 +570,19 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +[[package]] +name = "funcy" +version = "2.0" +description = "A fancy and practical functional tools" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"ctl\"" +files = [ + {file = "funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0"}, + {file = "funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb"}, +] + [[package]] name = "graphql-core" version = "3.2.4" @@ -796,6 +854,26 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jinja2-ansible-filters" +version = "1.3.2" +description = "A port of Ansible's jinja2 filters without requiring ansible core." +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"ctl\"" +files = [ + {file = "jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b"}, + {file = "jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34"}, +] + +[package.dependencies] +Jinja2 = "*" +PyYAML = "*" + +[package.extras] +test = ["pytest", "pytest-cov"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -1089,11 +1167,12 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +markers = {main = "extra == \"ctl\""} [[package]] name = "pexpect" @@ -1113,20 +1192,21 @@ ptyprocess = ">=0.5" [[package]] name = "platformdirs" -version = "4.3.3" +version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" -groups = ["dev"] +python-versions = ">=3.9" +groups = ["main", "dev"] files = [ - {file = "platformdirs-4.3.3-py3-none-any.whl", hash = "sha256:50a5450e2e84f44539718293cbb1da0a0885c9d14adf21b77bae4e66fc99d9b5"}, - {file = "platformdirs-4.3.3.tar.gz", hash = "sha256:d4e0b7d8ec176b341fb03cb11ca12d0276faa8c485f9cd218f613840463fc2c0"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] +markers = {main = "extra == \"ctl\""} [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" @@ -1144,6 +1224,28 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "plumbum" +version = "1.9.0" +description = "Plumbum: shell combinators library" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"ctl\"" +files = [ + {file = "plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5"}, + {file = "plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219"}, +] + +[package.dependencies] +pywin32 = {version = "*", markers = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +dev = ["coverage[toml]", "paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] +docs = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=1.0.0)"] +ssh = ["paramiko"] +test = ["coverage[toml]", "paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pytest-timeout"] + [[package]] name = "pprintpp" version = "0.4.0" @@ -1181,11 +1283,12 @@ version = "3.0.47" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, ] +markers = {main = "extra == \"ctl\""} [package.dependencies] wcwidth = "*" @@ -1608,8 +1711,7 @@ version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" -groups = ["dev"] -markers = "python_version >= \"3.10\" and sys_platform == \"win32\"" +groups = ["main", "dev"] files = [ {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, @@ -1630,6 +1732,7 @@ files = [ {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] +markers = {main = "platform_system == \"Windows\" and platform_python_implementation != \"PyPy\" and extra == \"ctl\"", dev = "python_version >= \"3.10\" and sys_platform == \"win32\""} [[package]] name = "pyyaml" @@ -1695,6 +1798,22 @@ files = [ ] markers = {main = "extra == \"ctl\" or extra == \"tests\" or extra == \"all\""} +[[package]] +name = "questionary" +version = "2.1.0" +description = "Python library to build pretty command line user prompts ⭐️" +optional = true +python-versions = ">=3.8" +groups = ["main"] +markers = "extra == \"ctl\"" +files = [ + {file = "questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec"}, + {file = "questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + [[package]] name = "requests" version = "2.32.3" @@ -2180,11 +2299,12 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, ] +markers = {main = "extra == \"ctl\""} [[package]] name = "whenever" @@ -2388,10 +2508,10 @@ type = ["pytest-mypy"] [extras] all = ["Jinja2", "numpy", "numpy", "pyarrow", "pytest", "pyyaml", "rich", "toml", "typer"] -ctl = ["Jinja2", "numpy", "numpy", "pyarrow", "pyyaml", "rich", "toml", "typer"] +ctl = ["Jinja2", "copier", "numpy", "numpy", "pyarrow", "pyyaml", "rich", "toml", "typer"] tests = ["Jinja2", "pytest", "pyyaml", "rich"] [metadata] lock-version = "2.1" python-versions = "^3.9, <3.14" -content-hash = "5e04cf55024fdf4f1c549eb7b17ef3791d8db85fa8447e92b4584f3d01b7f54c" +content-hash = "7b1d820d9eeaa5f3af9a7daed1a6ab16c98f0b4ce2e7abcf616379bb0d23e230" diff --git a/pyproject.toml b/pyproject.toml index c4b929e7..fe4f41c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ pytest = { version = "*", optional = true } pyyaml = { version = "^6", optional = true } eval-type-backport = { version = "^0.2.2", python = "~3.9" } dulwich = "^0.21.4" +copier = { version = "^9.5.0", optional = true} whenever = "0.7.2" netutils = "^1.0.0" @@ -68,7 +69,7 @@ infrahub-testcontainers = { version = "^1.2.5", python = ">=3.10" } astroid = "~3.1" [tool.poetry.extras] -ctl = ["Jinja2", "numpy", "pyarrow", "pyyaml", "rich", "toml", "typer"] +ctl = ["copier", "Jinja2", "numpy", "pyarrow", "pyyaml", "rich", "toml", "typer"] tests = ["Jinja2", "pytest", "pyyaml", "rich"] all = [ "Jinja2", @@ -106,6 +107,7 @@ addopts = "-vs --cov-report term-missing --cov-report xml --dist loadscope" pretty = true ignore_missing_imports = true disallow_untyped_defs = true +exclude = "\\{%.*%\\}" [[tool.mypy.overrides]] module = "infrahub_sdk.ctl.check" @@ -280,6 +282,11 @@ max-complexity = 17 "ANN204", # Missing return type annotation for special method ] +"infrahub_sdk/ctl/example_repo/**/*.py" = [ + "S101", # Use of assert detected + "N999", # Invalid module name +] + "tests/unit/sdk/test_client.py" = [ "W293", # Blank line contains whitespace (used within output check) ] diff --git a/tests/unit/ctl/test_repository_app.py b/tests/unit/ctl/test_repository_app.py index ba0a7721..58303434 100644 --- a/tests/unit/ctl/test_repository_app.py +++ b/tests/unit/ctl/test_repository_app.py @@ -1,8 +1,11 @@ """Integration tests for infrahubctl commands.""" +import tempfile +from pathlib import Path from unittest import mock import pytest +import yaml from typer.testing import CliRunner from infrahub_sdk.client import InfrahubClient @@ -322,3 +325,34 @@ def test_repo_list(self, mock_repositories_list) -> None: result = runner.invoke(app, ["repository", "list"]) assert result.exit_code == 0 assert strip_color(result.stdout) == read_fixture("output.txt", "integration/test_infrahubctl/repository_list") + + def test_repo_init(self) -> None: + with ( + tempfile.TemporaryDirectory() as temp_dst, + tempfile.NamedTemporaryFile(mode="w", suffix=".yml", delete=False, encoding="utf-8") as temp_yaml, + ): + dst = Path(temp_dst) + yaml_path = Path(temp_yaml.name) + + answers = { + "generators": True, + "menus": True, + "project_name": "test", + "queries": True, + "scripts": True, + "tests": True, + "transforms": True, + "package_mode": False, + } + + yaml.safe_dump(answers, temp_yaml) + temp_yaml.close() + runner.invoke(app, ["repository", "init", str(dst), "--data", str(yaml_path)]) + coppied_answers = yaml.safe_load((dst / ".copier-answers.yml").read_text()) + coppied_answers.pop("_src_path") + + assert coppied_answers == answers + assert (dst / "generators").is_dir() + assert (dst / "queries").is_dir() + assert (dst / "scripts").is_dir() + assert (dst / "pyproject.toml").is_file()