azure-vm-sdk 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- azure_vm_sdk-0.6.0/.editorconfig +23 -0
- azure_vm_sdk-0.6.0/.gitattributes +33 -0
- azure_vm_sdk-0.6.0/.github/workflows/ci.yml +97 -0
- azure_vm_sdk-0.6.0/.github/workflows/release.yml +66 -0
- azure_vm_sdk-0.6.0/.gitignore +22 -0
- azure_vm_sdk-0.6.0/.importlinter +33 -0
- azure_vm_sdk-0.6.0/.pre-commit-config.yaml +87 -0
- azure_vm_sdk-0.6.0/.python-version +1 -0
- azure_vm_sdk-0.6.0/CLAUDE.md +109 -0
- azure_vm_sdk-0.6.0/LICENSE +21 -0
- azure_vm_sdk-0.6.0/PKG-INFO +265 -0
- azure_vm_sdk-0.6.0/README.md +240 -0
- azure_vm_sdk-0.6.0/docs/superpowers/plans/2026-05-13-azure-vm-sdk-plan.md +2277 -0
- azure_vm_sdk-0.6.0/docs/superpowers/plans/2026-06-12-nsg-open-ports.md +157 -0
- azure_vm_sdk-0.6.0/docs/superpowers/specs/2026-05-13-azure-vm-sdk-design.md +322 -0
- azure_vm_sdk-0.6.0/pyproject.toml +185 -0
- azure_vm_sdk-0.6.0/src/azure_vm/__init__.py +32 -0
- azure_vm_sdk-0.6.0/src/azure_vm/_backend.py +131 -0
- azure_vm_sdk-0.6.0/src/azure_vm/_discovery.py +50 -0
- azure_vm_sdk-0.6.0/src/azure_vm/_templates.py +238 -0
- azure_vm_sdk-0.6.0/src/azure_vm/_workspace.py +175 -0
- azure_vm_sdk-0.6.0/src/azure_vm/client.py +302 -0
- azure_vm_sdk-0.6.0/src/azure_vm/devtools/__init__.py +6 -0
- azure_vm_sdk-0.6.0/src/azure_vm/devtools/code_eval.py +665 -0
- azure_vm_sdk-0.6.0/src/azure_vm/devtools/package_report.py +236 -0
- azure_vm_sdk-0.6.0/src/azure_vm/devtools/quality.py +36 -0
- azure_vm_sdk-0.6.0/src/azure_vm/e2e.py +250 -0
- azure_vm_sdk-0.6.0/src/azure_vm/exceptions.py +62 -0
- azure_vm_sdk-0.6.0/src/azure_vm/models.py +169 -0
- azure_vm_sdk-0.6.0/src/azure_vm/testing.py +5 -0
- azure_vm_sdk-0.6.0/src/azure_vm/vm.py +286 -0
- azure_vm_sdk-0.6.0/tests/__init__.py +0 -0
- azure_vm_sdk-0.6.0/tests/conftest.py +9 -0
- azure_vm_sdk-0.6.0/tests/integration/__init__.py +0 -0
- azure_vm_sdk-0.6.0/tests/integration/test_integration.py +107 -0
- azure_vm_sdk-0.6.0/tests/unit/__init__.py +0 -0
- azure_vm_sdk-0.6.0/tests/unit/devtools/__init__.py +0 -0
- azure_vm_sdk-0.6.0/tests/unit/devtools/test_code_eval.py +917 -0
- azure_vm_sdk-0.6.0/tests/unit/devtools/test_package_report.py +328 -0
- azure_vm_sdk-0.6.0/tests/unit/devtools/test_quality.py +102 -0
- azure_vm_sdk-0.6.0/tests/unit/test_backend.py +146 -0
- azure_vm_sdk-0.6.0/tests/unit/test_client.py +929 -0
- azure_vm_sdk-0.6.0/tests/unit/test_exceptions.py +48 -0
- azure_vm_sdk-0.6.0/tests/unit/test_models.py +76 -0
- azure_vm_sdk-0.6.0/tests/unit/test_open_ports.py +72 -0
- azure_vm_sdk-0.6.0/tests/unit/test_vm.py +565 -0
- azure_vm_sdk-0.6.0/tests/unit/test_workspace_refresh.py +95 -0
- azure_vm_sdk-0.6.0/uv.lock +1004 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# https://editorconfig.org
|
|
2
|
+
root = true
|
|
3
|
+
|
|
4
|
+
[*]
|
|
5
|
+
charset = utf-8
|
|
6
|
+
end_of_line = lf
|
|
7
|
+
insert_final_newline = true
|
|
8
|
+
trim_trailing_whitespace = true
|
|
9
|
+
indent_style = space
|
|
10
|
+
indent_size = 4
|
|
11
|
+
|
|
12
|
+
[*.{yml,yaml}]
|
|
13
|
+
indent_size = 2
|
|
14
|
+
|
|
15
|
+
[*.{toml,json,ini,cfg}]
|
|
16
|
+
indent_size = 2
|
|
17
|
+
|
|
18
|
+
[*.md]
|
|
19
|
+
# Trailing whitespace is meaningful in Markdown (hard line breaks).
|
|
20
|
+
trim_trailing_whitespace = false
|
|
21
|
+
|
|
22
|
+
[Makefile]
|
|
23
|
+
indent_style = tab
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Normalize line endings. This SDK is consumed from wherever the caller happens
|
|
2
|
+
# to be, so a CRLF leaked in from a Windows editor breaks things in ways that
|
|
3
|
+
# are hard to spot in review.
|
|
4
|
+
* text=auto eol=lf
|
|
5
|
+
|
|
6
|
+
*.py text diff=python
|
|
7
|
+
*.toml text
|
|
8
|
+
*.yml text
|
|
9
|
+
*.yaml text
|
|
10
|
+
*.json text
|
|
11
|
+
*.md text diff=markdown
|
|
12
|
+
*.txt text
|
|
13
|
+
*.ini text
|
|
14
|
+
*.cfg text
|
|
15
|
+
|
|
16
|
+
# Binary assets: never normalize, never try to diff.
|
|
17
|
+
*.png binary
|
|
18
|
+
*.jpg binary
|
|
19
|
+
*.jpeg binary
|
|
20
|
+
*.gif binary
|
|
21
|
+
*.ico binary
|
|
22
|
+
*.whl binary
|
|
23
|
+
*.gz binary
|
|
24
|
+
*.zip binary
|
|
25
|
+
|
|
26
|
+
# Generated at runtime.
|
|
27
|
+
.coverage linguist-generated=true
|
|
28
|
+
coverage.xml linguist-generated=true
|
|
29
|
+
# `linguist-generated` keeps this out of the language stats and collapses it in
|
|
30
|
+
# the GitHub diff view, which is right for a generated file. `-diff` is
|
|
31
|
+
# deliberately NOT set: a lockfile pins exact versions and hashes, so a change
|
|
32
|
+
# to it is exactly the kind of thing review should be able to see.
|
|
33
|
+
uv.lock linguist-generated=true
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Lint, format, types, security.
|
|
19
|
+
#
|
|
20
|
+
# Runs the same hooks as local development through pre-commit, so the two can
|
|
21
|
+
# not drift. This is the first CI this repository has had: it previously had
|
|
22
|
+
# no .github/workflows at all, so nothing was checked on push.
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
pre-commit:
|
|
25
|
+
name: pre-commit (ruff, basedpyright, bandit, import-linter)
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
|
|
30
|
+
- uses: actions/setup-python@v5
|
|
31
|
+
with:
|
|
32
|
+
# ruff's target-version is py311 and basedpyright is pointed at the
|
|
33
|
+
# 3.11 venv, so linting on the declared floor is the honest choice.
|
|
34
|
+
python-version: "3.11"
|
|
35
|
+
|
|
36
|
+
- uses: astral-sh/setup-uv@v5
|
|
37
|
+
with:
|
|
38
|
+
enable-cache: true
|
|
39
|
+
cache-dependency-glob: uv.lock
|
|
40
|
+
|
|
41
|
+
- name: Install the project
|
|
42
|
+
run: uv sync --frozen
|
|
43
|
+
|
|
44
|
+
- name: Install pre-commit
|
|
45
|
+
run: python -m pip install --upgrade pip pre-commit
|
|
46
|
+
|
|
47
|
+
- name: Cache pre-commit environments
|
|
48
|
+
uses: actions/cache@v4
|
|
49
|
+
with:
|
|
50
|
+
path: ~/.cache/pre-commit
|
|
51
|
+
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
|
52
|
+
|
|
53
|
+
- name: Run pre-commit
|
|
54
|
+
run: pre-commit run --all-files --show-diff-on-failure --color=always
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Tests + coverage gate.
|
|
58
|
+
#
|
|
59
|
+
# pyproject.toml sets `addopts = "-m 'not integration'"`, so the tests that
|
|
60
|
+
# need real Azure credentials and OpenTofu stay out of this job. The coverage
|
|
61
|
+
# threshold lives in [tool.coverage.report] and is deliberately not repeated
|
|
62
|
+
# as a --cov-fail-under flag here: stating it in two places is how the two
|
|
63
|
+
# come to disagree, with the command-line flag silently winning.
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
test:
|
|
66
|
+
name: pytest (py${{ matrix.python-version }})
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
strategy:
|
|
69
|
+
fail-fast: false
|
|
70
|
+
matrix:
|
|
71
|
+
# 3.11 is the declared floor (requires-python >=3.11).
|
|
72
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
73
|
+
steps:
|
|
74
|
+
- uses: actions/checkout@v4
|
|
75
|
+
|
|
76
|
+
- uses: actions/setup-python@v5
|
|
77
|
+
with:
|
|
78
|
+
python-version: ${{ matrix.python-version }}
|
|
79
|
+
|
|
80
|
+
- uses: astral-sh/setup-uv@v5
|
|
81
|
+
with:
|
|
82
|
+
enable-cache: true
|
|
83
|
+
cache-dependency-glob: uv.lock
|
|
84
|
+
|
|
85
|
+
- name: Install project
|
|
86
|
+
run: uv sync --frozen
|
|
87
|
+
|
|
88
|
+
- name: Run tests with coverage
|
|
89
|
+
run: uv run --frozen pytest --cov-report=xml
|
|
90
|
+
|
|
91
|
+
- name: Upload coverage artifact
|
|
92
|
+
if: always()
|
|
93
|
+
uses: actions/upload-artifact@v4
|
|
94
|
+
with:
|
|
95
|
+
name: coverage-${{ matrix.python-version }}
|
|
96
|
+
path: coverage.xml
|
|
97
|
+
if-no-files-found: ignore
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# A PyPI version can be yanked but never reused, so this only runs on a tag, and
|
|
4
|
+
# the tag is checked against the version the build actually produced before
|
|
5
|
+
# anything is uploaded.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
name: Build and publish azure-vm-sdk
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
# Must match the environment registered as this project's trusted publisher
|
|
18
|
+
# on PyPI, along with owner, repository and this filename.
|
|
19
|
+
environment: pypi
|
|
20
|
+
permissions:
|
|
21
|
+
# The whole credential: PyPI trusts a short-lived OIDC token minted here,
|
|
22
|
+
# so no API token is stored in the repository or in a secret.
|
|
23
|
+
id-token: write
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.11"
|
|
30
|
+
|
|
31
|
+
- uses: astral-sh/setup-uv@v5
|
|
32
|
+
with:
|
|
33
|
+
enable-cache: true
|
|
34
|
+
cache-dependency-glob: uv.lock
|
|
35
|
+
|
|
36
|
+
- name: Build azure-vm-sdk
|
|
37
|
+
run: uv build --out-dir dist
|
|
38
|
+
|
|
39
|
+
# The check that matters: uploading 0.7.0 under the tag v0.6.0 would burn
|
|
40
|
+
# the 0.7.0 version number permanently.
|
|
41
|
+
- name: Check the tag matches the built version
|
|
42
|
+
run: |
|
|
43
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
44
|
+
# Globs every wheel rather than "<name>-*.whl". The wheel is named after
|
|
45
|
+
# the distribution, not the module, and for azure-vm-sdk the two differ
|
|
46
|
+
# (`azure_vm_sdk` against `azure_vm`), so a module-based glob matched
|
|
47
|
+
# nothing and failed the build. One package per repository, so one wheel.
|
|
48
|
+
wheels=(dist/*.whl)
|
|
49
|
+
if [ "${#wheels[@]}" -ne 1 ]; then
|
|
50
|
+
echo "expected exactly one wheel in dist/, found ${#wheels[@]}" >&2
|
|
51
|
+
ls -la dist/ >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
artifact="$(basename "${wheels[0]}")"
|
|
55
|
+
case "$artifact" in
|
|
56
|
+
*"-$tag-"*) echo "tag $GITHUB_REF_NAME matches $artifact" ;;
|
|
57
|
+
*)
|
|
58
|
+
echo "tag $GITHUB_REF_NAME does not match the built $artifact" >&2
|
|
59
|
+
exit 1
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
|
|
63
|
+
- name: Publish to PyPI
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
65
|
+
with:
|
|
66
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
.pytest_cache/
|
|
3
|
+
.venv/
|
|
4
|
+
*.pyc
|
|
5
|
+
*.egg-info/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.svg
|
|
9
|
+
.pydeps/
|
|
10
|
+
.basedpyright/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.grimp_cache/
|
|
14
|
+
|
|
15
|
+
# Coverage reports. `uv.lock` is deliberately NOT ignored any more: this
|
|
16
|
+
# repository tracks it so CI can run `uv sync --frozen`, like the sibling
|
|
17
|
+
# projects. Leaving it ignored meant a clean checkout had no lockfile at all and
|
|
18
|
+
# `--frozen` could not be used.
|
|
19
|
+
.coverage
|
|
20
|
+
.coverage.*
|
|
21
|
+
coverage.xml
|
|
22
|
+
htmlcov/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[importlinter]
|
|
2
|
+
root_package = azure_vm
|
|
3
|
+
|
|
4
|
+
[importlinter:contract:models_and_exceptions_are_independent]
|
|
5
|
+
name = models and exceptions must not depend on other internal packages
|
|
6
|
+
type = forbidden
|
|
7
|
+
source_modules =
|
|
8
|
+
azure_vm.models
|
|
9
|
+
azure_vm.exceptions
|
|
10
|
+
forbidden_modules =
|
|
11
|
+
azure_vm._backend
|
|
12
|
+
azure_vm.vm
|
|
13
|
+
azure_vm.client
|
|
14
|
+
|
|
15
|
+
[importlinter:contract:backend_is_independent]
|
|
16
|
+
name = _backend must not depend on higher-level packages
|
|
17
|
+
type = forbidden
|
|
18
|
+
source_modules =
|
|
19
|
+
azure_vm._backend
|
|
20
|
+
forbidden_modules =
|
|
21
|
+
azure_vm.vm
|
|
22
|
+
azure_vm.client
|
|
23
|
+
|
|
24
|
+
[importlinter:contract:internal_modules_are_independent]
|
|
25
|
+
name = internal modules must not depend on public API layer
|
|
26
|
+
type = forbidden
|
|
27
|
+
source_modules =
|
|
28
|
+
azure_vm._templates
|
|
29
|
+
azure_vm._workspace
|
|
30
|
+
azure_vm._discovery
|
|
31
|
+
forbidden_modules =
|
|
32
|
+
azure_vm.client
|
|
33
|
+
azure_vm.vm
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Pre-commit hooks for azure-vm-sdk.
|
|
2
|
+
#
|
|
3
|
+
# uv run pre-commit install # enable on commit
|
|
4
|
+
# uv run pre-commit run --all-files # run everything once
|
|
5
|
+
#
|
|
6
|
+
# Version policy: the ruff revision below MUST stay in sync with the `ruff`
|
|
7
|
+
# pin in pyproject.toml. A newer ruff in pre-commit than in the dev environment
|
|
8
|
+
# makes the hook report violations that `uv run ruff check` does not.
|
|
9
|
+
#
|
|
10
|
+
# No default_language_version is set on purpose: pinning it to a specific
|
|
11
|
+
# python3.x makes pre-commit fail outright on a machine that does not have that
|
|
12
|
+
# interpreter on PATH, and the one hook that needs the project's interpreter
|
|
13
|
+
# (basedpyright) runs through `uv run --frozen` against the project venv anyway.
|
|
14
|
+
|
|
15
|
+
fail_fast: false
|
|
16
|
+
|
|
17
|
+
repos:
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Ruff - lint + format
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
22
|
+
rev: v0.16.6
|
|
23
|
+
hooks:
|
|
24
|
+
- id: ruff
|
|
25
|
+
name: ruff (lint)
|
|
26
|
+
args: [--fix, --exit-non-zero-on-fix]
|
|
27
|
+
- id: ruff-format
|
|
28
|
+
name: ruff (format)
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Type checking
|
|
32
|
+
#
|
|
33
|
+
# Runs through the project environment rather than a pre-commit mirror:
|
|
34
|
+
# basedpyright has to resolve `azure_vm` itself, and [tool.basedpyright]
|
|
35
|
+
# carries the include list and venv settings that make the pass meaningful.
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
- repo: local
|
|
38
|
+
hooks:
|
|
39
|
+
- id: basedpyright
|
|
40
|
+
name: basedpyright
|
|
41
|
+
entry: uv run --frozen basedpyright
|
|
42
|
+
language: system
|
|
43
|
+
pass_filenames: false
|
|
44
|
+
types: [python]
|
|
45
|
+
|
|
46
|
+
# The architectural contracts CI checks, run locally too.
|
|
47
|
+
- id: import-linter
|
|
48
|
+
name: import-linter
|
|
49
|
+
entry: uv run --frozen lint-imports --config .importlinter --no-cache
|
|
50
|
+
language: system
|
|
51
|
+
pass_filenames: false
|
|
52
|
+
files: ^src/
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Security
|
|
56
|
+
#
|
|
57
|
+
# This SDK drives an Azure VM over SSH and through OpenTofu, so subprocess
|
|
58
|
+
# and paramiko are its subject matter rather than a smell. B602 (shell=True)
|
|
59
|
+
# is deliberately not skipped anywhere: no call here uses it, and that must
|
|
60
|
+
# stay true.
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
- repo: https://github.com/PyCQA/bandit
|
|
63
|
+
rev: "1.9.4"
|
|
64
|
+
hooks:
|
|
65
|
+
- id: bandit
|
|
66
|
+
args: ["-c", "pyproject.toml"]
|
|
67
|
+
additional_dependencies: ["bandit[toml]"]
|
|
68
|
+
# pre-commit passes explicit filenames, which bypasses bandit's own
|
|
69
|
+
# exclude_dirs - so the test tree has to be excluded here as well.
|
|
70
|
+
exclude: ^tests/
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Generic file hygiene
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
76
|
+
rev: v6.0.0
|
|
77
|
+
hooks:
|
|
78
|
+
- id: check-toml
|
|
79
|
+
- id: check-yaml
|
|
80
|
+
- id: check-merge-conflict
|
|
81
|
+
- id: check-added-large-files
|
|
82
|
+
args: [--maxkb=1024]
|
|
83
|
+
- id: end-of-file-fixer
|
|
84
|
+
- id: trailing-whitespace
|
|
85
|
+
exclude: \.md$
|
|
86
|
+
- id: mixed-line-ending
|
|
87
|
+
args: [--fix=lf]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
A Pythonic SDK for managing Azure VMs through OpenTofu: a higher-level API over
|
|
8
|
+
a rendered OpenTofu workspace plus SSH access to the resulting machines. It is
|
|
9
|
+
consumed by `nanolab` through a git pin, so the public API in `azure_vm.__all__`
|
|
10
|
+
is a contract with that project.
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv sync
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires [uv](https://docs.astral.sh/uv/). Azure credentials and OpenTofu are
|
|
19
|
+
NOT required for the unit tests.
|
|
20
|
+
|
|
21
|
+
The toolchain (ruff, basedpyright, bandit, import-linter, pytest, pre-commit)
|
|
22
|
+
lives in `[dependency-groups].dev`, which a plain `uv sync` installs. It must
|
|
23
|
+
stay there and not move to `[project.optional-dependencies]`: extras are skipped
|
|
24
|
+
by `uv run`, so the basedpyright pre-commit hook — which runs `uv run --frozen
|
|
25
|
+
basedpyright` — would fail in CI with "Failed to spawn: basedpyright".
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Tests (the integration marker is deselected by addopts)
|
|
31
|
+
uv run pytest
|
|
32
|
+
uv run pytest tests/unit/test_vm.py::test_name -v
|
|
33
|
+
|
|
34
|
+
# Integration tests - need real Azure credentials and OpenTofu
|
|
35
|
+
uv run pytest -m integration -v
|
|
36
|
+
|
|
37
|
+
# Lint, format, types, security, contracts - the same hooks CI runs
|
|
38
|
+
uv run ruff check .
|
|
39
|
+
uv run ruff format .
|
|
40
|
+
uv run basedpyright
|
|
41
|
+
uv run bandit -c pyproject.toml -r src
|
|
42
|
+
uv run lint-imports --config .importlinter --no-cache
|
|
43
|
+
uv run pre-commit run --all-files
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`azure-vm-quality` runs ruff, basedpyright, bandit and import-linter together
|
|
47
|
+
and exits non-zero if any of them fails.
|
|
48
|
+
|
|
49
|
+
## Architecture
|
|
50
|
+
|
|
51
|
+
`src/azure_vm/` contains the package:
|
|
52
|
+
|
|
53
|
+
- `_backend.py` — the `TofuBackend` protocol and the subprocess implementation
|
|
54
|
+
that shells out to `tofu`. All process execution goes through a backend, which
|
|
55
|
+
is what makes the SDK testable without Azure.
|
|
56
|
+
- `_discovery.py` — locating the OpenTofu binary and the workspace root.
|
|
57
|
+
- `_templates.py` — rendering the `.tf` files for a VM.
|
|
58
|
+
- `_workspace.py` — the on-disk OpenTofu workspace: rendering, `init`, `apply`,
|
|
59
|
+
output parsing.
|
|
60
|
+
- `models.py` — dataclasses (`VmConfig`, `VmInfo`, `VmSize`, `ImageInfo`,
|
|
61
|
+
`VmState`, `VmArchitecture`, `CommandResult`, …).
|
|
62
|
+
- `exceptions.py` — typed hierarchy rooted at `AzureVmError`, plus
|
|
63
|
+
`SshConnectionError` and the rest, so callers catch a specific failure rather
|
|
64
|
+
than an opaque one.
|
|
65
|
+
- `vm.py` — `AzureVM`: the per-VM handle (info, start/stop/restart, delete,
|
|
66
|
+
clone, `exec`, `exec_structured`, `transfer`, `wait_for_ip`, `wait_ready`).
|
|
67
|
+
SSH goes through paramiko and commands are shell-quoted with `shlex.join`.
|
|
68
|
+
- `client.py` — `AzureClient`: cluster-level operations (launch, launch_many,
|
|
69
|
+
get_vm, ensure_running, list, find, list_sizes, purge).
|
|
70
|
+
- `testing.py` — `FakeTofuBackend` for consumers writing their own tests.
|
|
71
|
+
- `e2e.py` — the `azure-vm-e2e` console script: an end-to-end lifecycle
|
|
72
|
+
harness. Not imported by the library, and omitted from coverage.
|
|
73
|
+
- `devtools/` — repository tooling exposed as console scripts:
|
|
74
|
+
`azure-vm-quality`, `azure-vm-package-report` (import graph metrics via
|
|
75
|
+
`grimp`), `azure-vm-eval` (AST and coupling checks).
|
|
76
|
+
|
|
77
|
+
The architectural contracts live in `.importlinter`: `models` and `exceptions`
|
|
78
|
+
must not depend on the rest of the package, and the layers must not be imported
|
|
79
|
+
in the wrong direction. CI runs them, and so does pre-commit.
|
|
80
|
+
|
|
81
|
+
## Callers must not be broken
|
|
82
|
+
|
|
83
|
+
`nanolab` consumes this package through a git pin, so a change to a name in
|
|
84
|
+
`__all__`, to a signature, or to a returned model's attributes is a breaking
|
|
85
|
+
change for it even though nothing in this repository fails. Check that project
|
|
86
|
+
before renaming or removing a public symbol.
|
|
87
|
+
|
|
88
|
+
## Tooling
|
|
89
|
+
|
|
90
|
+
The same stack as the sibling projects.
|
|
91
|
+
|
|
92
|
+
| Concern | Tool | Config |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| Lint + format | ruff (88 cols) | `[tool.ruff]` |
|
|
95
|
+
| Types | basedpyright (standard) | `[tool.basedpyright]` |
|
|
96
|
+
| Security | bandit | `[tool.bandit]` |
|
|
97
|
+
| Import layers | import-linter | `.importlinter` |
|
|
98
|
+
| Coverage | pytest-cov | `[tool.coverage.report]`, `fail_under = 80` |
|
|
99
|
+
|
|
100
|
+
Notes that are easy to get wrong:
|
|
101
|
+
|
|
102
|
+
- `SLF` (flake8-self) is part of the rule set here, as it was before the
|
|
103
|
+
migration; the per-file-ignores record where private access is deliberate.
|
|
104
|
+
- This package supports Python 3.11, so `reportImplicitOverride` is off
|
|
105
|
+
(`typing.override` needs 3.12).
|
|
106
|
+
- `uv.lock` is tracked, so CI can run `uv sync --frozen`. It used to be
|
|
107
|
+
gitignored, which meant a clean checkout had no lockfile at all.
|
|
108
|
+
- `src/azure_vm/e2e.py` is omitted from coverage: it only runs against real
|
|
109
|
+
Azure.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 miciav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|