Skip to content

Commit 5d4e2a2

Browse files
authored
Merge pull request #179 from opsmill/develop
Merge develop into stable ahead of 1.2.0 release
2 parents 079ab59 + da1d89a commit 5d4e2a2

File tree

17 files changed

+732
-148
lines changed

17 files changed

+732
-148
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ jobs:
112112
./actionlint -color
113113
shell: bash
114114
env:
115-
SHELLCHECK_OPTS: --exclude=SC2086 --exclude=SC2046 --exclude=SC2004
115+
SHELLCHECK_OPTS: --exclude=SC2086 --exclude=SC2046 --exclude=SC2004 --exclude=SC2129
116116

117117

118118
unit-tests:
@@ -123,6 +123,7 @@ jobs:
123123
- "3.10"
124124
- "3.11"
125125
- "3.12"
126+
- "3.13"
126127
if: |
127128
always() && !cancelled() &&
128129
!contains(needs.*.result, 'failure') &&

.github/workflows/publish-python-sdk.yml renamed to .github/workflows/publish-pypi.yml

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
---
2-
name: Publish Infrahub Python SDK
2+
# yamllint disable rule:truthy
3+
name: Publish Infrahub SDK Package
34

4-
on: # yamllint disable rule:truthy
5-
push:
6-
tags:
7-
- "v*"
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
runs-on:
9+
description: "The OS to run the job on"
10+
required: false
11+
default: "ubuntu-22.04"
12+
type: string
13+
publish:
14+
type: boolean
15+
description: Whether to publish the package to Pypi
16+
required: false
17+
default: false
18+
workflow_call:
19+
inputs:
20+
runs-on:
21+
description: "The OS to run the job on"
22+
required: false
23+
default: "ubuntu-22.04"
24+
type: string
25+
publish:
26+
type: boolean
27+
description: Whether to publish the package to Pypi
28+
required: false
29+
default: false
830

931
jobs:
1032
publish_to_pypi:
@@ -25,6 +47,8 @@ jobs:
2547

2648
- name: "Check out repository code"
2749
uses: "actions/checkout@v4"
50+
with:
51+
submodules: true
2852

2953
- name: "Cache poetry venv"
3054
uses: "actions/cache@v4"
@@ -47,4 +71,5 @@ jobs:
4771
run: "ls -la dist/"
4872

4973
- name: "Poetry push PyPI"
74+
if: ${{ inputs.publish }}
5075
run: "poetry publish"

.github/workflows/release.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
# yamllint disable rule:truthy rule:line-length
3+
name: New Release
4+
5+
on:
6+
release:
7+
types:
8+
- published
9+
10+
jobs:
11+
check_release:
12+
runs-on: ubuntu-22.04
13+
outputs:
14+
is_prerelease: ${{ steps.release.outputs.is_prerelease }}
15+
is_devrelease: ${{ steps.release.outputs.is_devrelease }}
16+
version: ${{ steps.release.outputs.version }}
17+
major_minor_version: ${{ steps.release.outputs.major_minor_version }}
18+
latest_tag: ${{ steps.release.outputs.latest_tag }}
19+
steps:
20+
- name: "Check out repository code"
21+
uses: "actions/checkout@v4"
22+
with:
23+
submodules: true
24+
25+
- name: "Set up Python"
26+
uses: "actions/setup-python@v5"
27+
with:
28+
python-version: "3.12"
29+
30+
- name: "Install Poetry"
31+
uses: "snok/install-poetry@v1"
32+
with:
33+
virtualenvs-create: true
34+
virtualenvs-in-project: true
35+
installer-parallel: true
36+
37+
- name: "Setup Python environment"
38+
run: |
39+
poetry config virtualenvs.create true --local
40+
poetry env use 3.12
41+
- name: "Install dependencies"
42+
run: "poetry install --no-interaction --no-ansi"
43+
44+
- name: "Check prerelease type"
45+
id: release
46+
run: |
47+
echo is_prerelease=$(poetry run python -c "from packaging.version import Version; print(int(Version('$(poetry version -s)').is_prerelease))") >> "$GITHUB_OUTPUT"
48+
echo is_devrelease=$(poetry run python -c "from packaging.version import Version; print(int(Version('$(poetry version -s)').is_devrelease))") >> "$GITHUB_OUTPUT"
49+
echo "version=$(poetry version -s)" >> "$GITHUB_OUTPUT"
50+
echo major_minor_version=$(poetry run python -c "from packaging.version import Version; print(f\"{Version('$(poetry version -s)').major}.{Version('$(poetry version -s)').minor}\")") >> "$GITHUB_OUTPUT"
51+
echo latest_tag=$(curl -L \
52+
-H "Accept: application/vnd.github+json" \
53+
-H "Authorization: Bearer ${{ github.token }}" \
54+
-H "X-GitHub-Api-Version: 2022-11-28" \
55+
https://api.github.com/repos/${{ github.repository }}/releases/latest \
56+
| jq -r '.tag_name') >> "$GITHUB_OUTPUT"
57+
58+
- name: Check tag version
59+
if: github.event.release.tag_name != format('infrahub-v{0}', steps.release.outputs.version)
60+
run: |
61+
echo "Tag version does not match python project version"
62+
exit 1
63+
64+
- name: Check prerelease and project version
65+
if: github.event.release.prerelease == true && steps.release.outputs.is_prerelease == 0 && steps.release.outputs.is_devrelease == 0
66+
run: |
67+
echo "Cannot pre-release a non pre-release or non dev-release version (${{ steps.release.outputs.version }})"
68+
exit 1
69+
70+
- name: Check release and project version
71+
if: github.event.release.prerelease == false && (steps.release.outputs.is_prerelease == 1 || steps.release.outputs.is_devrelease == 1)
72+
run: |
73+
echo "Cannot release a pre-release or dev-release version (${{ steps.release.outputs.version }})"
74+
exit 1
75+
76+
publish-pypi:
77+
needs: check_release
78+
uses: ./.github/workflows/publish-pypi.yml
79+
secrets: inherit
80+
with:
81+
publish: true
82+
83+
update-submodule:
84+
needs: check_release
85+
uses: ./.github/workflows/update-submodule.yml
86+
secrets: inherit
87+
with:
88+
version: ${{ github.ref_name }}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
# yamllint disable rule:truthy
3+
name: Trigger Submodule update
4+
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
runs-on:
9+
description: "The OS to run the job on"
10+
required: false
11+
default: "ubuntu-22.04"
12+
type: string
13+
version:
14+
type: string
15+
required: false
16+
description: The string to extract semver from.
17+
default: ''
18+
workflow_call:
19+
inputs:
20+
runs-on:
21+
description: "The OS to run the job on"
22+
required: false
23+
default: "ubuntu-22.04"
24+
type: string
25+
version:
26+
type: string
27+
required: false
28+
description: The string to extract semver from.
29+
default: ''
30+
31+
jobs:
32+
trigger-submodule:
33+
runs-on: ubuntu-22.04
34+
steps:
35+
- name: Checkout code
36+
uses: actions/checkout@v4
37+
38+
- name: Trigger submodule update
39+
run: |
40+
echo "${{ inputs.version }}"
41+
curl -X POST \
42+
-H "Authorization: token ${{ secrets.GH_UPDATE_PACKAGE_OTTO }}" \
43+
-H "Accept: application/vnd.github.v3+json" \
44+
https://api.github.com/repos/opsmill/infrahub/dispatches \
45+
-d "{\"event_type\":\"trigger-submodule-update\", \"client_payload\": {\"version\": \"${{ inputs.version }}\"}}"

.yamllint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ rules:
1414
# See https://github.com/prettier/prettier/pull/10926 or https://github.com/redhat-developer/vscode-yaml/issues/433
1515
min-spaces-from-content: 1
1616
line-length:
17-
max: 120
17+
max: 140
1818
allow-non-breakable-words: true
1919
allow-non-breakable-inline-mappings: false

CHANGELOG.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,23 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang
1111

1212
<!-- towncrier release notes start -->
1313

14-
## [1.1.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.10.0) - 2024-11-28
14+
## [1.2.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.2.0) - 2024-12-19
1515

1616
### Added
1717

18-
- Added InfrahubClient.schema.wait_until_converged() which allowes you to wait until the schema has converged across all Infrahub workers before proceeding with an operation. The InfrahubClient.schema.load() method has also been updated with a new parameter "wait_until_converged".
18+
- Add batch feature, that use threading, to sync client ([#168](https://github.com/opsmill/infrahub-sdk-python/issues/168))
19+
- Added InfrahubClient.schema.in_sync method to indicate if a specific branch is in sync across all worker types
20+
- Added Python 3.13 to the list of supported versions
1921

2022
### Fixed
2123

22-
- CTL: `schema load` return a proper error message when authentication is missing or when the user doesn't have the permission to update the schema. ([#127](https://github.com/opsmill/infrahub-sdk-python/issues/127))
23-
- CTL: List available transforms and generators if no name is provided ([#140](https://github.com/opsmill/infrahub-sdk-python/issues/140))
24+
- Fix an issue with with `infrahubctl menu load` that would fail while loading the menu
2425

25-
## [1.1.0rc0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.1.0rc0) - 2024-11-26
26+
## [1.1.0](https://github.com/opsmill/infrahub-sdk-python/tree/v1.10.0) - 2024-11-28
27+
28+
### Added
29+
30+
- Added InfrahubClient.schema.wait_until_converged() which allowes you to wait until the schema has converged across all Infrahub workers before proceeding with an operation. The InfrahubClient.schema.load() method has also been updated with a new parameter "wait_until_converged".
2631

2732
### Fixed
2833

infrahub_sdk/batch.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import asyncio
22
from collections.abc import AsyncGenerator, Awaitable
3+
from concurrent.futures import ThreadPoolExecutor
34
from dataclasses import dataclass
4-
from typing import Any, Callable, Optional
5+
from typing import Any, Callable, Generator, Optional
56

6-
from .node import InfrahubNode
7+
from .node import InfrahubNode, InfrahubNodeSync
78

89

910
@dataclass
@@ -14,13 +15,32 @@ class BatchTask:
1415
node: Optional[Any] = None
1516

1617

18+
@dataclass
19+
class BatchTaskSync:
20+
task: Callable[..., Any]
21+
args: tuple[Any, ...]
22+
kwargs: dict[str, Any]
23+
node: Optional[InfrahubNodeSync] = None
24+
25+
def execute(self, return_exceptions: bool = False) -> tuple[Optional[InfrahubNodeSync], Any]:
26+
"""Executes the stored task."""
27+
result = None
28+
try:
29+
result = self.task(*self.args, **self.kwargs)
30+
except Exception as exc: # pylint: disable=broad-exception-caught
31+
if return_exceptions:
32+
return self.node, exc
33+
raise exc
34+
35+
return self.node, result
36+
37+
1738
async def execute_batch_task_in_pool(
1839
task: BatchTask, semaphore: asyncio.Semaphore, return_exceptions: bool = False
1940
) -> tuple[Optional[InfrahubNode], Any]:
2041
async with semaphore:
2142
try:
2243
result = await task.task(*task.args, **task.kwargs)
23-
2444
except Exception as exc: # pylint: disable=broad-exception-caught
2545
if return_exceptions:
2646
return (task.node, exc)
@@ -64,3 +84,26 @@ async def execute(self) -> AsyncGenerator:
6484
if isinstance(result, Exception) and not self.return_exceptions:
6585
raise result
6686
yield node, result
87+
88+
89+
class InfrahubBatchSync:
90+
def __init__(self, max_concurrent_execution: int = 5, return_exceptions: bool = False):
91+
self._tasks: list[BatchTaskSync] = []
92+
self.max_concurrent_execution = max_concurrent_execution
93+
self.return_exceptions = return_exceptions
94+
95+
@property
96+
def num_tasks(self) -> int:
97+
return len(self._tasks)
98+
99+
def add(self, *args: Any, task: Callable[..., Any], node: Optional[Any] = None, **kwargs: Any) -> None:
100+
self._tasks.append(BatchTaskSync(task=task, node=node, args=args, kwargs=kwargs))
101+
102+
def execute(self) -> Generator[tuple[Optional[InfrahubNodeSync], Any], None, None]:
103+
with ThreadPoolExecutor(max_workers=self.max_concurrent_execution) as executor:
104+
futures = [executor.submit(task.execute, return_exceptions=self.return_exceptions) for task in self._tasks]
105+
for future in futures:
106+
node, result = future.result()
107+
if isinstance(result, Exception) and not self.return_exceptions:
108+
raise result
109+
yield node, result

infrahub_sdk/client.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import ujson
2424
from typing_extensions import Self
2525

26-
from .batch import InfrahubBatch
26+
from .batch import InfrahubBatch, InfrahubBatchSync
2727
from .branch import (
2828
BranchData,
2929
InfrahubBranchManager,
@@ -1454,9 +1454,6 @@ def delete(self, kind: Union[str, type[SchemaTypeSync]], id: str, branch: Option
14541454
node = InfrahubNodeSync(client=self, schema=schema, branch=branch, data={"id": id})
14551455
node.delete()
14561456

1457-
def create_batch(self, return_exceptions: bool = False) -> InfrahubBatch:
1458-
raise NotImplementedError("This method hasn't been implemented in the sync client yet.")
1459-
14601457
def clone(self) -> InfrahubClientSync:
14611458
"""Return a cloned version of the client using the same configuration"""
14621459
return InfrahubClientSync(config=self.config)
@@ -1955,6 +1952,16 @@ def get(
19551952

19561953
return results[0]
19571954

1955+
def create_batch(self, return_exceptions: bool = False) -> InfrahubBatchSync:
1956+
"""Create a batch to execute multiple queries concurrently.
1957+
1958+
Executing the batch will be performed using a thread pool, meaning it cannot guarantee the execution order. It is not recommended to use such
1959+
batch to manipulate objects that depend on each others.
1960+
"""
1961+
return InfrahubBatchSync(
1962+
max_concurrent_execution=self.max_concurrent_execution, return_exceptions=return_exceptions
1963+
)
1964+
19581965
def get_list_repositories(
19591966
self, branches: Optional[dict[str, BranchData]] = None, kind: str = "CoreGenericRepository"
19601967
) -> dict[str, RepositoryData]:

infrahub_sdk/ctl/repository.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from pathlib import Path
2+
from typing import Optional
23

34
import typer
45
import yaml
56
from pydantic import ValidationError
67
from rich.console import Console
78

9+
from infrahub_sdk.ctl.client import initialize_client
10+
811
from ..async_typer import AsyncTyper
9-
from ..ctl.client import initialize_client
1012
from ..ctl.exceptions import FileNotValidError
1113
from ..ctl.utils import init_logging
1214
from ..graphql import Mutation
@@ -65,7 +67,7 @@ async def add(
6567
name: str,
6668
location: str,
6769
description: str = "",
68-
username: str = "",
70+
username: Optional[str] = None,
6971
password: str = "",
7072
commit: str = "",
7173
read_only: bool = False,
@@ -88,10 +90,9 @@ async def add(
8890

8991
client = initialize_client()
9092

91-
if username:
92-
credential = await client.create(kind="CorePasswordCredential", name=name, username=username, password=password)
93-
await credential.save()
94-
input_data["data"]["credential"] = {"id": credential.id}
93+
credential = await client.create(kind="CorePasswordCredential", name=name, username=username, password=password)
94+
await credential.save(allow_upsert=True)
95+
input_data["data"]["credential"] = {"id": credential.id}
9596

9697
query = Mutation(
9798
mutation="CoreReadOnlyRepositoryCreate" if read_only else "CoreRepositoryCreate",

infrahub_sdk/ctl/schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ async def load(
129129
for schema_file in schemas_data:
130130
console.print(f"[green] schema '{schema_file.location}' loaded successfully")
131131
else:
132-
console.print("[green] The schema in Infrahub was is already up to date, no changes were required")
132+
console.print("[green] The schema in Infrahub was already up to date, no changes were required")
133133

134134
console.print(f"[green] {len(schemas_data)} {schema_definition} processed in {loading_time:.3f} seconds.")
135135

0 commit comments

Comments
 (0)