diff --git a/.gitignore b/.gitignore index 89f3baa..f7479b6 100644 --- a/.gitignore +++ b/.gitignore @@ -181,9 +181,9 @@ cython_debug/ .abstra/ # Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, +# and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ @@ -226,4 +226,4 @@ Icon[] .AppleDesktop Network Trash Folder Temporary Items -.apdisk \ No newline at end of file +.apdisk diff --git a/.graphqlrc.yml b/.graphqlrc.yml deleted file mode 100644 index b54f8ac..0000000 --- a/.graphqlrc.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -schema: "http://localhost:8000/schema.graphql" -documents: "**/*.gql" diff --git a/.graphqlrc.yml.jinja b/.graphqlrc.yml.jinja new file mode 100644 index 0000000..c1cadda --- /dev/null +++ b/.graphqlrc.yml.jinja @@ -0,0 +1,3 @@ +--- +schema: "{{ infrahub_server_url }}/schema.graphql" +documents: "**/*.gql" diff --git a/.infrahub.yml b/.infrahub.yml deleted file mode 100644 index 27d3800..0000000 --- a/.infrahub.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -schemas: - - schemas diff --git a/.infrahub.yml.jinja b/.infrahub.yml.jinja new file mode 100644 index 0000000..37eec63 --- /dev/null +++ b/.infrahub.yml.jinja @@ -0,0 +1,51 @@ +# yaml-language-server: $schema=https://schema.infrahub.app/python-sdk/repository-config/latest.json +--- +# https://docs.infrahub.app/reference/dotinfrahub +schemas: # Schema definitions to load (files or directories) + - schemas + +{% if objects %} +objects: # Object definitions to load (files or directories) + - objects + +{% endif %} +{% if menus %} +menus: # Menu definitions to load (files or directories) + - menus + +{% endif %} +{% if transforms %} +jinja2_transforms: + - name: + query: + template_path: transforms/templates/.j2 + +python_transforms: + - name: + file_path: transforms/.py + +{% endif %} +{% if generators %} +generator_definitions: + - name: + file_path: generators/.py + query: + targets: + +{% endif %} +queries: + - name: + file_path: queries/.gql + +{% if checks %} +check_definitions: + - name: + file_path: checks/.py + +{% endif %} +artifact_definitions: + - name: + parameters: {} + content_type: + targets: + transformation: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..95a8a92 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +--- +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.10 + hooks: + # Run the linter. + - id: ruff-check + args: [--fix] + # Run the formatter. + - id: ruff-format + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.37.1 + hooks: + - id: yamllint + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + exclude: ^\.vscode/|/\.vscode/ + - id: mixed-line-ending + - id: trailing-whitespace diff --git a/.vscode/extensions.json b/.vscode/extensions.json index f2ed142..f4a9628 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,8 @@ { "recommendations": [ + "charliermarsh.ruff", + "ms-python.python", "opsmill.infrahub", + "redhat.vscode-yaml", ] } \ No newline at end of file diff --git a/.vscode/settings.json.jinja b/.vscode/settings.json.jinja new file mode 100644 index 0000000..71a4458 --- /dev/null +++ b/.vscode/settings.json.jinja @@ -0,0 +1,21 @@ +{ + "python.defaultInterpreterPath": ".venv/bin/python3", + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": true, + "infrahub-vscode.servers": [ + { + "name": "{{ project_name }}", + "address": "{{ infrahub_server_url }}", + "api_token": "{{ infrahub_api_token }}" + } + ], + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml", + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true + } +} \ No newline at end of file diff --git a/.yamllint.yml b/.yamllint.yml index 3114ed9..1c79fb5 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -14,6 +14,6 @@ rules: # See https://github.com/prettier/prettier/pull/10926 or https://github.com/redhat-developer/vscode-yaml/issues/433 min-spaces-from-content: 1 line-length: - max: 140 + max: 200 allow-non-breakable-words: true allow-non-breakable-inline-mappings: false diff --git a/README.md b/README.md index 0887c47..d8dbc74 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Infrahub Repository -Welcome! This repository was initialized via the `uv tool run --from 'infrahub-sdk[ctl]' infrahubctl repository init ` command. That bootstraps a repository for use with some example data. +[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-teal.json)](https://github.com/copier-org/copier) + +Welcome! This repository was initialized via the `uv tool run --from 'copier' copier copy https://github.com/opsmill/infrahub-template ` command. That bootstraps a repository for use with some example data. ## Installation diff --git a/copier.yml b/copier.yml index adab042..d4dc0cc 100644 --- a/copier.yml +++ b/copier.yml @@ -1,37 +1,99 @@ --- +_envops: + lstrip_blocks: true + trim_blocks: true + project_name: type: str - help: The name for your new Infrahub repository. This will be used as the package name. + help: Name for this Infrahub repository (also used as the package name). + validator: >- + {% if not (project_name | regex_search('^[a-z][a-z0-9\-]+$')) %} + project_name must start with a letter, followed one or more letters, digits or dashes all lowercase. + {% endif %} + qmark: ๐Ÿ“” + +is_local_development: + type: bool + default: true + help: Is this repository intended for local development only? If false, additional configuration options will be requested. + qmark: ๐Ÿ—๏ธ + +default_infrahub_branch: + # Only ask if not a local development project + when: "{{ not is_local_development }}" + type: str + nullable: true + help: Branch name to use instead of the server default. Leave empty to use the server default. + validator: >- + {% if not default_infrahub_branch and (default_infrahub_branch | regex_search('^[A-Za-z][A-Za-z0-9\-_]+$')) %} + default_infrahub_branch must start with a letter, followed one or more letters, digits or dashes. + {% endif %} + qmark: ๐ŸŒฒ + +infrahub_server_url: + # Only ask if not a local development project + when: "{{ not is_local_development }}" + type: str + default: http://localhost:8000 + help: URL of the Infrahub server (e.g., http://host:port or https://host). + validator: >- + {% if infrahub_server_url and not (infrahub_server_url | regex_search('^https?:\/\/[A-Za-z][A-Za-z0-9\-_]+(?::(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]?\d{1,4}))?$')) %} + Invalid url. Must be something like `http://:` or `https://` + {% endif %} + qmark: ๐Ÿ–ฅ๏ธ + +infrahub_api_token: + # Only ask if not a local development project + when: "{{ not is_local_development }}" + type: str + default: 06438eb2-8019-4776-878c-0941b1f1d1ec + help: API token for authentication. + qmark: ๐Ÿ” + objects: type: bool - help: >- - Enable support for Infrahub object files. default: false + help: Include support for object files (preloads data entries). + qmark: ๐Ÿ—ƒ๏ธ + generators: type: bool - help: >- - Enable support for Infrahub data generators. - Generators are plugins that create objects based on input data in Infrahub. default: false + help: Include support for data generators (plugins that create objects from input data). + qmark: โš™๏ธ + transforms: type: bool - help: >- - Enable support for Infrahub data transforms. - A Transformation is a generic plugin to transform a dataset into a different format to simplify it's ingestion by third-party systems. default: false + help: Include support for data transforms (Jinja2 & Python plugins that convert data for third-party systems). + qmark: ๐Ÿ—๏ธ + scripts: type: bool - help: Include a 'scripts/' directory for custom automation scripts that interact with the Infrahub API. default: false + help: Include a `scripts/` directory for custom automation scripts." + qmark: ๐Ÿ“œ + menus: type: bool - help: Include a 'menus/' directory and configuration to define custom navigation menus in the Infrahub UI. default: false + help: Include a `menus/` directory for custom UI navigation menus. + qmark: โš™๏ธ + +checks: + type: bool + default: false + help: Include a `checks/` directory for custom validation logic. + qmark: โœ… + tests: type: bool - help: Set up a Python testing environment with pytest for integration testing your schemas and data. default: false + help: Include a pytest environment for integration testing schemas and data. + qmark: ๐Ÿงช + package_mode: type: bool - help: Initialize the repository as an installable Python package (with pyproject.toml). default: false + help: Configure as an installable Python package with pyproject.toml. + qmark: ๐Ÿ“ฆ diff --git a/infrahubctl.toml.jinja b/infrahubctl.toml.jinja new file mode 100644 index 0000000..c270901 --- /dev/null +++ b/infrahubctl.toml.jinja @@ -0,0 +1,5 @@ +server_address="{{ infrahub_server_url }}" +api_token="{{ infrahub_api_token }}" +{% if infrahub_default_branch and infrahub_default_branch not in ["", "main"] %} +default_branch="{{ infrahub_default_branch }}" +{% endif %} diff --git a/pyproject.toml.jinja b/pyproject.toml.jinja index 8a67896..a0ce4fb 100644 --- a/pyproject.toml.jinja +++ b/pyproject.toml.jinja @@ -6,28 +6,26 @@ authors = [] readme = "README.md" requires-python = ">=3.11, <3.14" dependencies = [ - "infrahub-sdk[all]>=1.13.1", - "invoke>=2.2.0", + "httpx>=0.27.2", + "infrahub-sdk[all]>=1.17.0", + "invoke>=2.2.1", ] [dependency-groups] dev = [ - "infrahub-testcontainers>=1.3.0", - "pytest>=8.4.1", - "pytest-asyncio>=1.0.0", - "ruff>=0.12.0", + "infrahub-testcontainers>=1.6.2", + "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", + "ruff>=0.14.10", "mypy>=1.17.1", - "pytest>=8.4.1", - "pytest-asyncio>=1.0.0", - "ruff>=0.12.0", "yamllint>=1.37.1", + "pre-commit>=4.5.1", ] [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] - [tool.mypy] pretty = true ignore_missing_imports = true @@ -47,7 +45,6 @@ exclude = [ "examples", ] - [tool.ruff.lint] preview = true @@ -71,7 +68,6 @@ ignore = [ "PLR", ] - #https://docs.astral.sh/ruff/formatter/black/ [tool.ruff.format] quote-style = "double" @@ -84,4 +80,4 @@ max-line-length = 150 [tool.ruff.lint.mccabe] # Target max-complexity=10 -max-complexity = 17 \ No newline at end of file +max-complexity = 17 diff --git a/schemas/example.yml b/schemas/example.yml index 3d1ab68..ddc396f 100644 --- a/schemas/example.yml +++ b/schemas/example.yml @@ -4,7 +4,7 @@ version: "1.0" nodes: - name: Device namespace: Network - human_friendly_id: ['hostname__value'] + human_friendly_id: ["hostname__value"] attributes: - name: hostname kind: Text diff --git a/tasks.py b/tasks.py index 6991d6d..1e725f5 100644 --- a/tasks.py +++ b/tasks.py @@ -1,13 +1,15 @@ +"""Tasks for managing InfraHub services and operations.""" + import os from pathlib import Path import httpx -from invoke import Context, task +from invoke import Collection, Context, task # If no version is indicated, we will take the latest -VERSION = os.getenv("INFRAHUB_IMAGE_VER", None) -CURRENT_DIRECTORY = Path(__file__).resolve() -MAIN_DIRECTORY_PATH = Path(__file__).parent +VERSION: str | None = os.getenv(key="INFRAHUB_IMAGE_VER") +CURRENT_DIRECTORY: Path = Path(__file__).resolve() +MAIN_DIRECTORY_PATH: Path = Path(__file__).parent @task @@ -16,7 +18,10 @@ 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") + compose_start_cmd = "docker compose up -d" + if VERSION: + compose_start_cmd = f"{VERSION=} {compose_start_cmd}" + context.run(compose_start_cmd) @task @@ -51,44 +56,60 @@ def restart(context: Context, component: str = "") -> None: @task -def load_menu(ctx: Context) -> None: +def load_menu(context: Context) -> None: """ Load schemas into InfraHub using infrahubctl. """ - ctx.run("infrahubctl menu load menus/", pty=True) + context.run("infrahubctl menu load menus/", pty=True) @task -def load_schema(ctx: Context) -> None: +def load_schema(context: Context) -> None: """ Load schemas into InfraHub using infrahubctl. """ - ctx.run("infrahubctl schema load schemas") + context.run("infrahubctl schema load schemas") @task -def load_objects(ctx: Context) -> None: +def load_objects(context: Context) -> None: """ Load objects into InfraHub using infrahubctl. """ - ctx.run("infrahubctl object load objects") + context.run("infrahubctl object load objects") @task -def test(ctx: Context) -> None: +def test(context: Context) -> None: """ Run tests using pytest. """ - ctx.run("pytest tests") + context.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 +@task( + name="get-compose-file", + help={ + "override": "Download the file even if it already exists.", + "version": "Version of the docker-compose.yml to download. (should match the version of the image)", + "emma": "Download the version with Emma included", + }, +) +def download_compose_file( + context: Context, # noqa: ARG001 + override: bool = False, + version: str | None = None, + emma: bool = False, +) -> Path: """ Download docker-compose.yml from InfraHub if missing or override is True. """ - compose_file = Path("./docker-compose.yml") - compose_url = os.getenv("INFRAHUB_COMPOSE_URL", "https://infrahub.opsmill.io") + compose_file: Path = Path("./docker-compose.yml") + compose_url: str = os.getenv(key="INFRAHUB_COMPOSE_URL", default="https://infrahub.opsmill.io") + if version: + compose_url = f"{compose_url}/{version}" + if emma: + compose_url = f"{compose_url}-emma" if compose_file.exists() and not override: return compose_file @@ -96,52 +117,71 @@ def download_compose_file(context: Context, override: bool = False) -> Path: # response = httpx.get(compose_url) response.raise_for_status() - with compose_file.open("w", encoding="utf-8") as f: - f.write(response.content.decode()) + compose_file.write_text(response.content.decode(), encoding="utf-8") return compose_file -@task(name="format") -def format_python(ctx: Context) -> None: +@task +def format_python(context: Context) -> None: """Run RUFF to format all Python files.""" exec_cmds = ["ruff format .", "ruff check . --fix"] - with ctx.cd(MAIN_DIRECTORY_PATH): + with context.cd(MAIN_DIRECTORY_PATH): for cmd in exec_cmds: - ctx.run(cmd, pty=True) + context.run(cmd, pty=True) @task -def lint_yaml(ctx: Context) -> None: +def lint_yaml(context: Context) -> None: """Run Linter to check all Python files.""" print(" - Check code with yamllint") exec_cmd = "yamllint ." - with ctx.cd(MAIN_DIRECTORY_PATH): - ctx.run(exec_cmd, pty=True) + with context.cd(MAIN_DIRECTORY_PATH): + context.run(exec_cmd, pty=True) @task -def lint_mypy(ctx: Context) -> None: +def lint_mypy(context: Context) -> None: """Run Linter to check all Python files.""" print(" - Check code with mypy") exec_cmd = "mypy --show-error-codes infrahub_sdk" - with ctx.cd(MAIN_DIRECTORY_PATH): - ctx.run(exec_cmd, pty=True) + with context.cd(MAIN_DIRECTORY_PATH): + context.run(exec_cmd, pty=True) @task -def lint_ruff(ctx: Context) -> None: +def lint_ruff(context: Context) -> None: """Run Linter to check all Python files.""" print(" - Check code with ruff") exec_cmd = "ruff check ." - with ctx.cd(MAIN_DIRECTORY_PATH): - ctx.run(exec_cmd, pty=True) + with context.cd(MAIN_DIRECTORY_PATH): + context.run(exec_cmd, pty=True) @task(name="lint") -def lint_all(ctx: Context) -> None: +def lint_all(context: Context) -> None: """Run all linters.""" - lint_yaml(ctx) - lint_ruff(ctx) - lint_mypy(ctx) + lint_yaml(context) + lint_ruff(context) + lint_mypy(context) + + +@task(name="format") +def format_all(context: Context) -> None: + """Run all formatters.""" + format_python(context) + # TODO: yaml formatting + + +docker_ns = Collection( + "docker", start, stop, restart, destroy, download_compose_file +) +format_ns = Collection("format", python=format_python) +format_ns.add_task(format_all, name="all", default=True) +lint_ns = Collection("lint", yaml=lint_yaml, mypy=lint_mypy, ruff=lint_ruff) +lint_ns.add_task(lint_all, name="all", default=True) +load_ns = Collection( + "load", menu=load_menu, schema=load_schema, objects=load_objects +) +ns = Collection(docker_ns, format_ns, lint_ns, load_ns, test) diff --git a/{% if checks %}checks{% endif %}/.gitkeep b/{% if checks %}checks{% endif %}/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/{% if objects %}objects{% endif %}/example.yml b/{% if objects %}objects{% endif %}/example.yml index a844223..49a0cb2 100644 --- a/{% if objects %}objects{% endif %}/example.yml +++ b/{% if objects %}objects{% endif %}/example.yml @@ -5,4 +5,4 @@ spec: kind: BuiltinTag data: - name: Yellow - description: A bright color often associated with happiness and energy. \ No newline at end of file + description: A bright color often associated with happiness and energy. diff --git a/{% if package_mode %}lib{% endif %}/example.py b/{% if package_mode %}lib{% endif %}/example.py index cfb4b3a..f5ef482 100644 --- a/{% if package_mode %}lib{% endif %}/example.py +++ b/{% if package_mode %}lib{% endif %}/example.py @@ -3,6 +3,7 @@ from infrahub_sdk.node import InfrahubNode -def print_nodes(log: logging.Logger, nodes: list[InfrahubNode]): - for node in nodes.keys(): +def print_nodes(log: logging.Logger, nodes: list[InfrahubNode]) -> None: + """Print all nodes in the provided list.""" + for node in nodes: log.info(f"{node} present.") diff --git a/{% if scripts %}scripts{% endif %}/example_script.py b/{% if scripts %}scripts{% endif %}/example_script.py index 84bf690..fdb6b6a 100644 --- a/{% if scripts %}scripts{% endif %}/example_script.py +++ b/{% if scripts %}scripts{% endif %}/example_script.py @@ -1,15 +1,16 @@ import logging -from lib.example import print_nodes - from infrahub_sdk import InfrahubClient +from lib.example import print_nodes + async def run( client: InfrahubClient, log: logging.Logger, branch: str, -): +) -> None: + """Print all nodes in the current branch.""" log.info(f"Running example script on {branch}...") nodes = await client.schema.all() print_nodes(log, nodes) diff --git a/{% if tests %}tests{% endif %}/integration/conftest.py b/{% if tests %}tests{% endif %}/integration/conftest.py index 3bd0a17..f3231f2 100644 --- a/{% if tests %}tests{% endif %}/integration/conftest.py +++ b/{% if tests %}tests{% endif %}/integration/conftest.py @@ -2,7 +2,6 @@ from typing import Any import pytest - from infrahub_sdk.yaml import SchemaFile CURRENT_DIRECTORY = Path(__file__).parent.resolve() diff --git a/{% if tests %}tests{% endif %}/integration/test_infrahub.py b/{% if tests %}tests{% endif %}/integration/test_infrahub.py index 330487f..99d933c 100644 --- a/{% if tests %}tests{% endif %}/integration/test_infrahub.py +++ b/{% if tests %}tests{% endif %}/integration/test_infrahub.py @@ -1,7 +1,6 @@ from pathlib import Path import pytest - from infrahub_sdk import InfrahubClient from infrahub_sdk.protocols import CoreGenericRepository from infrahub_sdk.testing.docker import TestInfrahubDockerClient @@ -12,7 +11,7 @@ class TestInfrahub(TestInfrahubDockerClient): @pytest.mark.asyncio async def test_load_schema( self, default_branch: str, client: InfrahubClient, schemas: list[dict] - ): + ) -> None: await client.schema.wait_until_converged(branch=default_branch) resp = await client.schema.load( diff --git a/{{_copier_conf.answers_file}}.jinja b/{{ _copier_conf.answers_file }}.jinja similarity index 54% rename from {{_copier_conf.answers_file}}.jinja rename to {{ _copier_conf.answers_file }}.jinja index ea97bd4..3d2f8bf 100644 --- a/{{_copier_conf.answers_file}}.jinja +++ b/{{ _copier_conf.answers_file }}.jinja @@ -1,2 +1,2 @@ # Changes here will be overwritten by Copier -{{ _copier_answers|to_nice_yaml -}} \ No newline at end of file +{{ _copier_answers | to_nice_yaml -}}