From 306b68a281c175f7a1fe914a3ba4bc7cbb372db8 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:19:00 -0500 Subject: [PATCH 01/24] bump dependencies & add httpx --- pyproject.toml.jinja | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyproject.toml.jinja b/pyproject.toml.jinja index 8a67896..fb01083 100644 --- a/pyproject.toml.jinja +++ b/pyproject.toml.jinja @@ -6,20 +6,18 @@ authors = [] readme = "README.md" requires-python = ">=3.11, <3.14" dependencies = [ - "infrahub-sdk[all]>=1.13.1", - "invoke>=2.2.0", + "infrahub-sdk[all]>=1.17.0", + "invoke>=2.2.1", + "httpx>=0.28.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", ] From 9a09a09e8f2e9ee073244f72a120cd50fa0b5a4c Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:20:12 -0500 Subject: [PATCH 02/24] end of file & quote linting --- .gitignore | 2 +- pyproject.toml.jinja | 2 +- schemas/example.yml | 2 +- {% if objects %}objects{% endif %}/example.yml | 2 +- {{_copier_conf.answers_file}}.jinja | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 89f3baa..233f96a 100644 --- a/.gitignore +++ b/.gitignore @@ -226,4 +226,4 @@ Icon[] .AppleDesktop Network Trash Folder Temporary Items -.apdisk \ No newline at end of file +.apdisk diff --git a/pyproject.toml.jinja b/pyproject.toml.jinja index fb01083..231ce0b 100644 --- a/pyproject.toml.jinja +++ b/pyproject.toml.jinja @@ -82,4 +82,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/{% 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/{{_copier_conf.answers_file}}.jinja b/{{_copier_conf.answers_file}}.jinja index ea97bd4..a96840d 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 -}} From 87581a5b307ce851f23192ad1c2107067c36de70 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:20:33 -0500 Subject: [PATCH 03/24] update command in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0887c47..26e491e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. +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 From 67073c3df37577f69f1b3adea07667ca4e62b331 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:21:32 -0500 Subject: [PATCH 04/24] linting task.py --- tasks.py | 66 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/tasks.py b/tasks.py index 6991d6d..65cd2bd 100644 --- a/tasks.py +++ b/tasks.py @@ -1,3 +1,4 @@ +"""Tasks for managing InfraHub services and operations.""" import os from pathlib import Path @@ -5,9 +6,9 @@ from invoke import 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("INFRAHUB_IMAGE_VER") +CURRENT_DIRECTORY: Path = Path(__file__).resolve() +MAIN_DIRECTORY_PATH: Path = Path(__file__).parent @task @@ -51,44 +52,49 @@ 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( + help={"override": "Redownload the compose file even if it already exists."} +) +def download_compose_file(context: Context, override: bool = False) -> Path: """ Download docker-compose.yml from InfraHub if missing or override is True. """ + _ = context compose_file = Path("./docker-compose.yml") - compose_url = os.getenv("INFRAHUB_COMPOSE_URL", "https://infrahub.opsmill.io") + compose_url = os.getenv( + "INFRAHUB_COMPOSE_URL", "https://infrahub.opsmill.io" + ) if compose_file.exists() and not override: return compose_file @@ -103,45 +109,45 @@ def download_compose_file(context: Context, override: bool = False) -> Path: # @task(name="format") -def format_python(ctx: Context) -> None: +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) From 6b38aae3f1e0d8fa76b1380cc9f574cd571d4e6d Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:21:52 -0500 Subject: [PATCH 05/24] use version var in docker compose --- tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tasks.py b/tasks.py index 65cd2bd..f0833fc 100644 --- a/tasks.py +++ b/tasks.py @@ -17,7 +17,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 From 1bbf56b99c54e191ccfcb2efd0f769fc408ac069 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:53:44 -0500 Subject: [PATCH 06/24] add yaml language server to infrahub config file --- .infrahub.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.infrahub.yml b/.infrahub.yml index 27d3800..66d2ed9 100644 --- a/.infrahub.yml +++ b/.infrahub.yml @@ -1,3 +1,6 @@ +# yaml-language-server: $schema=https://schema.infrahub.app/python-sdk/repository-config/latest.json --- -schemas: +# https://docs.infrahub.app/reference/dotinfrahub#check-definitions + +schemas: # Schema definitions to load (files or directories) - schemas From 1b6be06bed712fe59b3fafd444f0531c2c2d246b Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:56:13 -0500 Subject: [PATCH 07/24] add relevant vscode extensions --- .vscode/extensions.json | 3 +++ 1 file changed, 3 insertions(+) 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 From 12255f50eb2a7279408d91ba73e9964b643bc209 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 09:59:51 -0500 Subject: [PATCH 08/24] j2 linting --- ...nswers_file}}.jinja => {{ _copier_conf.answers_file }}.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {{_copier_conf.answers_file}}.jinja => {{ _copier_conf.answers_file }}.jinja (54%) 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 a96840d..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 -}} +{{ _copier_answers | to_nice_yaml -}} From 097d2f9782239c835febbf058a652b8007e8a8e6 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 10:03:32 -0500 Subject: [PATCH 09/24] add check definitions --- copier.yml | 4 ++++ {% if checks %}checks{% endif %} copy/.gitkeep | 0 2 files changed, 4 insertions(+) create mode 100644 {% if checks %}checks{% endif %} copy/.gitkeep diff --git a/copier.yml b/copier.yml index adab042..c742160 100644 --- a/copier.yml +++ b/copier.yml @@ -27,6 +27,10 @@ menus: type: bool help: Include a 'menus/' directory and configuration to define custom navigation menus in the Infrahub UI. default: false +checks: + type: bool + help: Include a 'checks/' directory to define custom validation (check definitions) logic for your Infrahub objects. + default: false tests: type: bool help: Set up a Python testing environment with pytest for integration testing your schemas and data. diff --git a/{% if checks %}checks{% endif %} copy/.gitkeep b/{% if checks %}checks{% endif %} copy/.gitkeep new file mode 100644 index 0000000..e69de29 From 0b453be29662cbf17ae7140574f5d91b3b44cd00 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 10:05:56 -0500 Subject: [PATCH 10/24] add templates --- {% if transforms %}templates{% endif %}/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 {% if transforms %}templates{% endif %}/.gitkeep diff --git a/{% if transforms %}templates{% endif %}/.gitkeep b/{% if transforms %}templates{% endif %}/.gitkeep new file mode 100644 index 0000000..e69de29 From 4e6b6c3f6d39186f840fc24b2132be40238bf79d Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 12:03:56 -0500 Subject: [PATCH 11/24] linting and formating tasks.py --- tasks.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tasks.py b/tasks.py index f0833fc..b12f17f 100644 --- a/tasks.py +++ b/tasks.py @@ -1,4 +1,5 @@ """Tasks for managing InfraHub services and operations.""" + import os from pathlib import Path @@ -6,7 +7,7 @@ from invoke import Context, task # If no version is indicated, we will take the latest -VERSION: str | None = os.getenv("INFRAHUB_IMAGE_VER") +VERSION: str | None = os.getenv(key="INFRAHUB_IMAGE_VER") CURRENT_DIRECTORY: Path = Path(__file__).resolve() MAIN_DIRECTORY_PATH: Path = Path(__file__).parent @@ -16,7 +17,7 @@ def start(context: Context) -> None: """ Start the services using docker-compose in detached mode. """ - download_compose_file(context, override=False) + download_compose_file(override=False) compose_start_cmd = "docker compose up -d" if VERSION: compose_start_cmd = f"{VERSION=} {compose_start_cmd}" @@ -28,7 +29,7 @@ def destroy(context: Context) -> None: """ Stop and remove containers, networks, and volumes. """ - download_compose_file(context, override=False) + download_compose_file(override=False) context.run("docker compose down -v") @@ -37,7 +38,7 @@ def stop(context: Context) -> None: """ Stop containers and remove networks. """ - download_compose_file(context, override=False) + download_compose_file(override=False) context.run("docker compose down") @@ -46,7 +47,7 @@ def restart(context: Context, component: str = "") -> None: """ Restart all services or a specific one using docker-compose. """ - download_compose_file(context, override=False) + download_compose_file(override=False) if component: context.run(f"docker compose restart {component}") return @@ -86,17 +87,14 @@ def test(context: Context) -> None: 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: +@task(help={"override": "Download the file even if it already exists."}) +def download_compose_file(override: bool = False) -> Path: """ Download docker-compose.yml from InfraHub if missing or override is True. """ - _ = context - 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 compose_file.exists() and not override: From 33ea2bf7999d7cd28ad8fd163492d4ae86ae82ff Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 12:04:10 -0500 Subject: [PATCH 12/24] add copier badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 26e491e..500bc7c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Infrahub Repository +[![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 From a8040a8d80b565e2e2bca85b44487b4234532744 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 12:04:46 -0500 Subject: [PATCH 13/24] fix checks folder --- .../.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {{% if checks %}checks{% endif %} copy => {% if checks %}checks{% endif %}}/.gitkeep (100%) diff --git a/{% if checks %}checks{% endif %} copy/.gitkeep b/{% if checks %}checks{% endif %}/.gitkeep similarity index 100% rename from {% if checks %}checks{% endif %} copy/.gitkeep rename to {% if checks %}checks{% endif %}/.gitkeep From b6fefdb0d4bcbd67a9ad650034b826ecef57262e Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 12:18:54 -0500 Subject: [PATCH 14/24] remove templates since included in transforms --- {% if transforms %}templates{% endif %}/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 {% if transforms %}templates{% endif %}/.gitkeep diff --git a/{% if transforms %}templates{% endif %}/.gitkeep b/{% if transforms %}templates{% endif %}/.gitkeep deleted file mode 100644 index e69de29..0000000 From 29ab6434d96536bae10d0c2f21a611b25ffc9d49 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 12:45:01 -0500 Subject: [PATCH 15/24] expand copier templating --- .graphqlrc.yml | 3 -- .graphqlrc.yml.jinja | 3 ++ .infrahub.yml | 6 --- .infrahub.yml.jinja | 51 ++++++++++++++++++++++ .vscode/settings.json.jinja | 15 +++++++ copier.yml | 86 +++++++++++++++++++++++++++++++------ infrahubctl.toml.jinja | 5 +++ pyproject.toml.jinja | 3 -- 8 files changed, 146 insertions(+), 26 deletions(-) delete mode 100644 .graphqlrc.yml create mode 100644 .graphqlrc.yml.jinja delete mode 100644 .infrahub.yml create mode 100644 .infrahub.yml.jinja create mode 100644 .vscode/settings.json.jinja create mode 100644 infrahubctl.toml.jinja 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 66d2ed9..0000000 --- a/.infrahub.yml +++ /dev/null @@ -1,6 +0,0 @@ -# yaml-language-server: $schema=https://schema.infrahub.app/python-sdk/repository-config/latest.json ---- -# https://docs.infrahub.app/reference/dotinfrahub#check-definitions - -schemas: # Schema definitions to load (files or directories) - - schemas diff --git a/.infrahub.yml.jinja b/.infrahub.yml.jinja new file mode 100644 index 0000000..7ae952f --- /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#check-definitions +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/.vscode/settings.json.jinja b/.vscode/settings.json.jinja new file mode 100644 index 0000000..ccc2db6 --- /dev/null +++ b/.vscode/settings.json.jinja @@ -0,0 +1,15 @@ +{ + "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/copier.yml b/copier.yml index c742160..b94aecb 100644 --- a/copier.yml +++ b/copier.yml @@ -1,41 +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 of your new Infrahub repository. Used as the Python 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: Default branch to use for this Infrahub repository. Only set this to override 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 this repository targets, for example `http://:` or `https://`. Defaults to local development. + 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 used to authenticate with the Infrahub server. Defaults to the development token. + qmark: ๐Ÿ” + objects: type: bool - help: >- - Enable support for Infrahub object files. default: false + help: Enable support for Infrahub object definitions. + 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: Enable support for Infrahub data generators. Generators are plugins that create objects based on input data in Infrahub. + 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: Enable support for Infrahub data transforms (Jinja2 and Python) to convert datasets for downstream consumption. + 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 that interacts with the Infrahub API. + 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 to define custom navigation menus in the Infrahub UI. + qmark: โš™๏ธ + checks: type: bool - help: Include a 'checks/' directory to define custom validation (check definitions) logic for your Infrahub objects. default: false + help: Include a `checks/` directory for custom validation logic for Infrahub objects. + 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 `tests/` directory and set up a pytest-based test environment for validating schemas and data. + qmark: ๐Ÿงช + package_mode: type: bool - help: Initialize the repository as an installable Python package (with pyproject.toml). default: false + help: Initialize the repository as an installable Python package using `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 231ce0b..a99008b 100644 --- a/pyproject.toml.jinja +++ b/pyproject.toml.jinja @@ -25,7 +25,6 @@ dev = [ asyncio_mode = "auto" testpaths = ["tests"] - [tool.mypy] pretty = true ignore_missing_imports = true @@ -45,7 +44,6 @@ exclude = [ "examples", ] - [tool.ruff.lint] preview = true @@ -69,7 +67,6 @@ ignore = [ "PLR", ] - #https://docs.astral.sh/ruff/formatter/black/ [tool.ruff.format] quote-style = "double" From b158b3381eb2ca3fd8d71b7dded93e967d77cfc8 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 12:47:07 -0500 Subject: [PATCH 16/24] remove url typo --- .infrahub.yml.jinja | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.infrahub.yml.jinja b/.infrahub.yml.jinja index 7ae952f..37eec63 100644 --- a/.infrahub.yml.jinja +++ b/.infrahub.yml.jinja @@ -1,6 +1,6 @@ # yaml-language-server: $schema=https://schema.infrahub.app/python-sdk/repository-config/latest.json --- -# https://docs.infrahub.app/reference/dotinfrahub#check-definitions +# https://docs.infrahub.app/reference/dotinfrahub schemas: # Schema definitions to load (files or directories) - schemas From 0f63800a7139208a1c2a7a7e60340c6b33947f09 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 13:01:59 -0500 Subject: [PATCH 17/24] further linting --- .gitignore | 4 ++-- .pre-commit-config.yaml | 24 ++++++++++++++++++++++++ .yamllint.yml | 2 +- copier.yml | 4 ++-- pyproject.toml.jinja | 1 + 5 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.gitignore b/.gitignore index 233f96a..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/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..990fc64 --- /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/.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/copier.yml b/copier.yml index b94aecb..fc0ec83 100644 --- a/copier.yml +++ b/copier.yml @@ -1,7 +1,7 @@ --- _envops: - lstrip_blocks: true - trim_blocks: true + lstrip_blocks: true + trim_blocks: true project_name: type: str diff --git a/pyproject.toml.jinja b/pyproject.toml.jinja index a99008b..42b2181 100644 --- a/pyproject.toml.jinja +++ b/pyproject.toml.jinja @@ -19,6 +19,7 @@ dev = [ "ruff>=0.14.10", "mypy>=1.17.1", "yamllint>=1.37.1", + "pre-commit>=14.5.1", ] [tool.pytest.ini_options] From fe3f7f168c1d3bb74904e7e9da3b3fd7641e9096 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 13:08:13 -0500 Subject: [PATCH 18/24] refine the copier propts to make more concise --- copier.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/copier.yml b/copier.yml index fc0ec83..d4dc0cc 100644 --- a/copier.yml +++ b/copier.yml @@ -5,7 +5,7 @@ _envops: project_name: type: str - help: Name of your new Infrahub repository. Used as the Python 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. @@ -23,7 +23,7 @@ default_infrahub_branch: when: "{{ not is_local_development }}" type: str nullable: true - help: Default branch to use for this Infrahub repository. Only set this to override the server default. + 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. @@ -35,7 +35,7 @@ infrahub_server_url: when: "{{ not is_local_development }}" type: str default: http://localhost:8000 - help: URL of the Infrahub server this repository targets, for example `http://:` or `https://`. Defaults to local development. + 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://` @@ -47,53 +47,53 @@ infrahub_api_token: when: "{{ not is_local_development }}" type: str default: 06438eb2-8019-4776-878c-0941b1f1d1ec - help: API token used to authenticate with the Infrahub server. Defaults to the development token. + help: API token for authentication. qmark: ๐Ÿ” objects: type: bool default: false - help: Enable support for Infrahub object definitions. + help: Include support for object files (preloads data entries). qmark: ๐Ÿ—ƒ๏ธ generators: type: bool default: false - help: Enable support for Infrahub data generators. Generators are plugins that create objects based on input data in Infrahub. + help: Include support for data generators (plugins that create objects from input data). qmark: โš™๏ธ transforms: type: bool default: false - help: Enable support for Infrahub data transforms (Jinja2 and Python) to convert datasets for downstream consumption. + help: Include support for data transforms (Jinja2 & Python plugins that convert data for third-party systems). qmark: ๐Ÿ—๏ธ scripts: type: bool default: false - help: Include a `scripts/` directory for custom automation that interacts with the Infrahub API. + help: Include a `scripts/` directory for custom automation scripts." qmark: ๐Ÿ“œ menus: type: bool default: false - help: Include a `menus/` directory to define custom navigation menus in the Infrahub UI. + 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 for Infrahub objects. + help: Include a `checks/` directory for custom validation logic. qmark: โœ… tests: type: bool default: false - help: Include a `tests/` directory and set up a pytest-based test environment for validating schemas and data. + help: Include a pytest environment for integration testing schemas and data. qmark: ๐Ÿงช package_mode: type: bool default: false - help: Initialize the repository as an installable Python package using `pyproject.toml`. + help: Configure as an installable Python package with pyproject.toml. qmark: ๐Ÿ“ฆ From ca25193bc3466318786e272ce62aad2f88eb0eca Mon Sep 17 00:00:00 2001 From: Ryan Merolle Date: Fri, 2 Jan 2026 13:18:27 -0500 Subject: [PATCH 19/24] Fix httpx and pre-commit dependencies --- pyproject.toml.jinja | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml.jinja b/pyproject.toml.jinja index 42b2181..a0ce4fb 100644 --- a/pyproject.toml.jinja +++ b/pyproject.toml.jinja @@ -6,9 +6,9 @@ authors = [] readme = "README.md" requires-python = ">=3.11, <3.14" dependencies = [ + "httpx>=0.27.2", "infrahub-sdk[all]>=1.17.0", "invoke>=2.2.1", - "httpx>=0.28.1", ] [dependency-groups] @@ -19,7 +19,7 @@ dev = [ "ruff>=0.14.10", "mypy>=1.17.1", "yamllint>=1.37.1", - "pre-commit>=14.5.1", + "pre-commit>=4.5.1", ] [tool.pytest.ini_options] From 7b7633a13572082dece10fe9ed0ba576e908b588 Mon Sep 17 00:00:00 2001 From: Ryan Merolle Date: Fri, 2 Jan 2026 13:37:29 -0500 Subject: [PATCH 20/24] correct context, add Emma, and version to download_compose_file --- tasks.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/tasks.py b/tasks.py index b12f17f..6a76f77 100644 --- a/tasks.py +++ b/tasks.py @@ -17,7 +17,7 @@ def start(context: Context) -> None: """ Start the services using docker-compose in detached mode. """ - download_compose_file(override=False) + download_compose_file(context, override=False) compose_start_cmd = "docker compose up -d" if VERSION: compose_start_cmd = f"{VERSION=} {compose_start_cmd}" @@ -29,7 +29,7 @@ def destroy(context: Context) -> None: """ Stop and remove containers, networks, and volumes. """ - download_compose_file(override=False) + download_compose_file(context, override=False) context.run("docker compose down -v") @@ -38,7 +38,7 @@ def stop(context: Context) -> None: """ Stop containers and remove networks. """ - download_compose_file(override=False) + download_compose_file(context, override=False) context.run("docker compose down") @@ -47,7 +47,7 @@ def restart(context: Context, component: str = "") -> None: """ Restart all services or a specific one using docker-compose. """ - download_compose_file(override=False) + download_compose_file(context, override=False) if component: context.run(f"docker compose restart {component}") return @@ -87,15 +87,28 @@ def test(context: Context) -> None: context.run("pytest tests") -@task(help={"override": "Download the file even if it already exists."}) -def download_compose_file(override: bool = False) -> Path: +@task( + 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 = Path("./docker-compose.yml") - compose_url: str = os.getenv( - key="INFRAHUB_COMPOSE_URL", default="https://infrahub.opsmill.io" - ) + 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 @@ -103,8 +116,7 @@ def download_compose_file(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 From e8c973d383eb6204a36a5961d30b0608c74f8dfe Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 13:44:47 -0500 Subject: [PATCH 21/24] further linting --- .pre-commit-config.yaml | 2 +- tasks.py | 4 +++- {% if package_mode %}lib{% endif %}/example.py | 5 +++-- {% if scripts %}scripts{% endif %}/example_script.py | 7 ++++--- {% if tests %}tests{% endif %}/integration/conftest.py | 1 - .../integration/test_infrahub.py | 3 +-- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 990fc64..95a8a92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: hooks: # Run the linter. - id: ruff-check - args: [ --fix ] + args: [--fix] # Run the formatter. - id: ruff-format - repo: https://github.com/adrienverge/yamllint.git diff --git a/tasks.py b/tasks.py index 6a76f77..79d604f 100644 --- a/tasks.py +++ b/tasks.py @@ -104,7 +104,9 @@ def download_compose_file( Download docker-compose.yml from InfraHub if missing or override is True. """ compose_file: Path = Path("./docker-compose.yml") - compose_url: str = os.getenv(key="INFRAHUB_COMPOSE_URL", default="https://infrahub.opsmill.io") + compose_url: str = os.getenv( + key="INFRAHUB_COMPOSE_URL", default="https://infrahub.opsmill.io" + ) if version: compose_url = f"{compose_url}/{version}" if emma: 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( From 89fb36e7b135c4bcf33d84ee84af5a3e9fad9412 Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 14:55:59 -0500 Subject: [PATCH 22/24] group tasks by collection --- tasks.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/tasks.py b/tasks.py index 79d604f..1e725f5 100644 --- a/tasks.py +++ b/tasks.py @@ -4,7 +4,7 @@ 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: str | None = os.getenv(key="INFRAHUB_IMAGE_VER") @@ -88,11 +88,12 @@ def test(context: Context) -> None: @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 @@ -104,9 +105,7 @@ def download_compose_file( Download docker-compose.yml from InfraHub if missing or override is True. """ compose_file: Path = Path("./docker-compose.yml") - compose_url: str = os.getenv( - key="INFRAHUB_COMPOSE_URL", default="https://infrahub.opsmill.io" - ) + compose_url: str = os.getenv(key="INFRAHUB_COMPOSE_URL", default="https://infrahub.opsmill.io") if version: compose_url = f"{compose_url}/{version}" if emma: @@ -123,7 +122,7 @@ def download_compose_file( return compose_file -@task(name="format") +@task def format_python(context: Context) -> None: """Run RUFF to format all Python files.""" @@ -166,3 +165,23 @@ def lint_all(context: Context) -> None: 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) From 3441ebae75bd56dbb87d79f46800a70b50aa461f Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 14:56:55 -0500 Subject: [PATCH 23/24] add Interpreter settings to vscode to use venv --- .vscode/settings.json.jinja | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.vscode/settings.json.jinja b/.vscode/settings.json.jinja index ccc2db6..71a4458 100644 --- a/.vscode/settings.json.jinja +++ b/.vscode/settings.json.jinja @@ -1,4 +1,10 @@ { + "python.defaultInterpreterPath": ".venv/bin/python3", + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": true, "infrahub-vscode.servers": [ { "name": "{{ project_name }}", From 47d4f71b0de2e3ba9d2bf99e46988ee7da4ed52b Mon Sep 17 00:00:00 2001 From: ryanmerolle Date: Fri, 2 Jan 2026 15:06:10 -0500 Subject: [PATCH 24/24] update the readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 500bc7c..d8dbc74 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![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. +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