make-azure 0.4.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.
@@ -0,0 +1,19 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .env
10
+
11
+ # wrangler caches its Pages upload state here (site/ is deployed by direct upload).
12
+ .wrangler/
13
+
14
+ # The VS Code extension in editors/vscode: `dist/` is above, and these two are what
15
+ # building a .vsix leaves behind. `npm install` is never run there -- the extension has
16
+ # no runtime dependency and both CLIs come from `npx --yes` -- so node_modules/ is
17
+ # listed to catch the day somebody tries.
18
+ node_modules/
19
+ *.vsix
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.5
2
+ Name: make-azure
3
+ Version: 0.4.0
4
+ Summary: A free-tier Azure VM for mkrun -- nearest region, DuckDNS name, nothing billed left behind; no az CLI.
5
+ Project-URL: Homepage, https://make.optersoft.com
6
+ Project-URL: Documentation, https://academy.optersoft.com/cloud/azure/virtual-machine
7
+ Author-email: "Optersoft, S.L." <david@optersoft.com>
8
+ License-Expression: MIT OR Apache-2.0
9
+ Keywords: azure,duckdns,free tier,mk,mkrun,virtual machine
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: azure-identity>=1.19
12
+ Requires-Dist: mkrun>=0.4.3
13
+ Description-Content-Type: text/markdown
14
+
15
+ # make-azure
16
+
17
+ A **free-tier Azure virtual machine** for [mkrun](https://pypi.org/project/mkrun/): created in
18
+ the nearest region that will build it, optionally given a DuckDNS name, and deleted with
19
+ everything it made, so nothing is left billing.
20
+
21
+ Sign-in is Microsoft's `azure-identity`: a browser, MFA up front, tokens in the encrypted
22
+ system keychain. Everything else is plain Resource Manager HTTPS. There's no `az` CLI and
23
+ no `azure-mgmt-*` SDK.
24
+
25
+ **Documentation:** [Virtual machine — Optersoft Academy](https://academy.optersoft.com/cloud/azure/virtual-machine)
26
+
27
+ ## From any shell
28
+
29
+ ```console
30
+ $ uv tool install mkrun
31
+ $ mk --install azure # make-azure, beside mk
32
+ $ mk azure.login --tenant <tenant> # once; a browser opens
33
+ $ mk azure.account # which subscription, and is it a free one
34
+ $ mk azure.region # the nearest regions, and the one azure.vm uses
35
+ $ mk azure.vm box # Ubuntu 24.04, SSH open
36
+ $ mk azure.vm box --duckdns mybox # ...and mybox.duckdns.org points at it
37
+ $ mk azure.vm box --no-public-ip # private network only; avoids the billed IPv4
38
+ $ mk azure.vms
39
+ $ mk azure.dns box mybox
40
+ $ mk azure.delete box
41
+ $ mk --dry-run azure.vm box # the plan; no sign-in, nothing sent
42
+ ```
43
+
44
+ With no task file in the directory, `mk` runs the tasks of the packages installed with
45
+ `mk --install`. Inside a project that has a `Makefile.py`, the file decides what is loaded;
46
+ use `mk --global azure.vm box` there.
47
+
48
+ ## In a task file
49
+
50
+ ```python
51
+ # /// script
52
+ # dependencies = ["make-azure"]
53
+ # ///
54
+ from make_azure import azure
55
+ ```
56
+
57
+ ## What is free
58
+
59
+ The free account (and Azure for Students) covers 750 hours a month of `Standard_B2ats_v2`
60
+ (the default), `Standard_B2pts_v2` or `Standard_B1s`, and a 64 GiB P6 disk, for its first 12
61
+ months. A Standard public IPv4 is **billed**, about $3.65 a month: `--no-public-ip` leaves it
62
+ out. A subscription without free services is refused unless `--paid-ok` is given.
63
+
64
+ ## Settings
65
+
66
+ | variable | what |
67
+ |---|---|
68
+ | `DUCKDNS_TOKEN` | for `--duckdns` / `azure.dns`; a credential, so it's withheld and redacted |
69
+ | `SSH_PUBLIC_KEY` | the key itself (CI); else `~/.ssh/id_ed25519.pub` or `id_rsa.pub` |
70
+ | `AZURE_CLIENT_ID` + `AZURE_TENANT_ID` | sign in as an app, with `AZURE_FEDERATED_TOKEN` (CI OIDC), `AZURE_FEDERATED_TOKEN_FILE` or `AZURE_CLIENT_SECRET` |
71
+
72
+ They come from the environment or `~/.make/secrets.env`. The sign-in is remembered in
73
+ `~/.make/azure/login.json`; that file holds no secret.
74
+
75
+ ## Licence
76
+
77
+ MIT OR Apache-2.0.
@@ -0,0 +1,63 @@
1
+ # make-azure
2
+
3
+ A **free-tier Azure virtual machine** for [mkrun](https://pypi.org/project/mkrun/): created in
4
+ the nearest region that will build it, optionally given a DuckDNS name, and deleted with
5
+ everything it made, so nothing is left billing.
6
+
7
+ Sign-in is Microsoft's `azure-identity`: a browser, MFA up front, tokens in the encrypted
8
+ system keychain. Everything else is plain Resource Manager HTTPS. There's no `az` CLI and
9
+ no `azure-mgmt-*` SDK.
10
+
11
+ **Documentation:** [Virtual machine — Optersoft Academy](https://academy.optersoft.com/cloud/azure/virtual-machine)
12
+
13
+ ## From any shell
14
+
15
+ ```console
16
+ $ uv tool install mkrun
17
+ $ mk --install azure # make-azure, beside mk
18
+ $ mk azure.login --tenant <tenant> # once; a browser opens
19
+ $ mk azure.account # which subscription, and is it a free one
20
+ $ mk azure.region # the nearest regions, and the one azure.vm uses
21
+ $ mk azure.vm box # Ubuntu 24.04, SSH open
22
+ $ mk azure.vm box --duckdns mybox # ...and mybox.duckdns.org points at it
23
+ $ mk azure.vm box --no-public-ip # private network only; avoids the billed IPv4
24
+ $ mk azure.vms
25
+ $ mk azure.dns box mybox
26
+ $ mk azure.delete box
27
+ $ mk --dry-run azure.vm box # the plan; no sign-in, nothing sent
28
+ ```
29
+
30
+ With no task file in the directory, `mk` runs the tasks of the packages installed with
31
+ `mk --install`. Inside a project that has a `Makefile.py`, the file decides what is loaded;
32
+ use `mk --global azure.vm box` there.
33
+
34
+ ## In a task file
35
+
36
+ ```python
37
+ # /// script
38
+ # dependencies = ["make-azure"]
39
+ # ///
40
+ from make_azure import azure
41
+ ```
42
+
43
+ ## What is free
44
+
45
+ The free account (and Azure for Students) covers 750 hours a month of `Standard_B2ats_v2`
46
+ (the default), `Standard_B2pts_v2` or `Standard_B1s`, and a 64 GiB P6 disk, for its first 12
47
+ months. A Standard public IPv4 is **billed**, about $3.65 a month: `--no-public-ip` leaves it
48
+ out. A subscription without free services is refused unless `--paid-ok` is given.
49
+
50
+ ## Settings
51
+
52
+ | variable | what |
53
+ |---|---|
54
+ | `DUCKDNS_TOKEN` | for `--duckdns` / `azure.dns`; a credential, so it's withheld and redacted |
55
+ | `SSH_PUBLIC_KEY` | the key itself (CI); else `~/.ssh/id_ed25519.pub` or `id_rsa.pub` |
56
+ | `AZURE_CLIENT_ID` + `AZURE_TENANT_ID` | sign in as an app, with `AZURE_FEDERATED_TOKEN` (CI OIDC), `AZURE_FEDERATED_TOKEN_FILE` or `AZURE_CLIENT_SECRET` |
57
+
58
+ They come from the environment or `~/.make/secrets.env`. The sign-in is remembered in
59
+ `~/.make/azure/login.json`; that file holds no secret.
60
+
61
+ ## Licence
62
+
63
+ MIT OR Apache-2.0.
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "make-azure"
3
+ # 0.4.0, not 0.1.0: 0.1-0.3 were published and then deleted, and PyPI never
4
+ # lets a filename be used twice.
5
+ version = "0.4.0"
6
+ description = "A free-tier Azure VM for mkrun -- nearest region, DuckDNS name, nothing billed left behind; no az CLI."
7
+ readme = "README.md"
8
+ requires-python = ">=3.11"
9
+ # Same dual licence as mkrun. The implementation came from optersoft's
10
+ # azure-virtual-machine (AGPL-3.0, Optersoft its only author), relicensed here.
11
+ license = "MIT OR Apache-2.0"
12
+ authors = [{ name = "Optersoft, S.L.", email = "david@optersoft.com" }]
13
+ keywords = ["azure", "virtual machine", "free tier", "duckdns", "mk", "mkrun"]
14
+ # azure-identity owns the sign-in (MSAL, the encrypted token cache) and nothing
15
+ # else: the API calls are plain REST through `make.http`. Not the azure-mgmt-*
16
+ # SDKs -- more packages and a slow import to wrap a handful of URLs. It is
17
+ # imported lazily, so `mk --list` never pays for it. mkrun 0.4.3 is the first
18
+ # that loads `mkrun.groups` entry points with no task file.
19
+ dependencies = ["mkrun>=0.4.3", "azure-identity>=1.19"]
20
+
21
+ # What lets `mk azure.vm` run with no task file, once `mk --install azure`
22
+ # has put this package beside mk. The value is the module that registers the group.
23
+ [project.entry-points."mkrun.groups"]
24
+ azure = "make_azure.azure"
25
+
26
+ [build-system]
27
+ requires = ["hatchling"]
28
+ build-backend = "hatchling.build"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/make_azure"]
32
+
33
+ [project.urls]
34
+ Homepage = "https://make.optersoft.com"
35
+ Documentation = "https://academy.optersoft.com/cloud/azure/virtual-machine"
36
+
37
+ [tool.uv.sources]
38
+ mkrun = { workspace = true }
39
+
40
+ [dependency-groups]
41
+ dev = ["pytest>=8", "ruff>=0.6"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ addopts = "-q"
46
+
47
+ # No [tool.ruff] here on purpose: every member shares the root config.
@@ -0,0 +1,36 @@
1
+ """Azure tasks for mkrun -- the `azure` group.
2
+
3
+ One thing: a virtual machine on Azure's free tier, in the nearest region that
4
+ will build it, optionally with a DuckDNS name, and removed with everything it
5
+ made. Sign-in is azure-identity (a browser, MFA up front, an encrypted token
6
+ cache); the rest is plain Resource Manager REST through `make.http`.
7
+
8
+ From any shell, with no task file:
9
+
10
+ uv tool install mkrun
11
+ mk --install azure
12
+ mk azure.vm box
13
+
14
+ Or in a task file, where importing is what registers it:
15
+
16
+ # Makefile.py
17
+ from make_azure import azure
18
+
19
+ The group merges with a repo's own `azure.*` tasks -- groups are namespaces,
20
+ and this package claims only its seven names.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ __version__ = "0.4.0"
26
+
27
+ __all__ = ["azure", "vm"]
28
+
29
+
30
+ def __getattr__(name: str):
31
+ """Import on first access, keeping module scope import-free -- startup is a feature."""
32
+ if name in __all__:
33
+ import importlib
34
+
35
+ return importlib.import_module(f".{name}", __name__)
36
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,113 @@
1
+ """The `azure` group: a free-tier virtual machine, made and removed.
2
+
3
+ mk azure.login --tenant T sign in once (a browser opens)
4
+ mk azure.account which subscription, and whether it is free
5
+ mk azure.region the nearest regions, and the one azure.vm uses
6
+ mk azure.vm box Ubuntu 24.04, SSH open, in the nearest region
7
+ mk azure.vm box --duckdns mybox ...and mybox.duckdns.org points at it
8
+ mk azure.vms what the subscription has
9
+ mk azure.dns box mybox point mybox.duckdns.org at an existing VM
10
+ mk azure.delete box the VM and everything it made
11
+ mk --dry-run azure.vm box the plan; nothing is sent, no sign-in
12
+
13
+ The work is in `make_azure.vm`, imported inside each task so `mk --list` never
14
+ loads azure-identity. No `az` CLI is involved.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Annotated
20
+
21
+ from make import arg, group
22
+
23
+ __all__ = ["azure"]
24
+
25
+ azure = group("azure")
26
+
27
+ Subscription = Annotated[str, arg(help="subscription id or name, if the sign-in reaches several")]
28
+ Size = Annotated[str, arg(help="Standard_B2ats_v2 (default), Standard_B2pts_v2 or Standard_B1s")]
29
+ Origin = Annotated[str | None, arg(help="your position, LAT,LON; default: from the time zone")]
30
+
31
+
32
+ @azure.task(name="login")
33
+ def login(
34
+ *,
35
+ tenant: Annotated[str, arg(help="tenant id or domain; needed for a personal account")] = "organizations",
36
+ ) -> None:
37
+ """Sign in with a browser, MFA included; tokens go in the encrypted cache."""
38
+ from . import vm
39
+
40
+ vm.login(tenant)
41
+
42
+
43
+ @azure.task(name="account")
44
+ def account(*, subscription: Subscription = "") -> None:
45
+ """The subscription this sign-in acts in, and whether its offer carries free services."""
46
+ from . import vm
47
+
48
+ vm.account(subscription_name=subscription)
49
+
50
+
51
+ @azure.task(name="region")
52
+ def region(
53
+ *, size: Size = "Standard_B2ats_v2", origin: Origin = None, subscription: Subscription = ""
54
+ ) -> None:
55
+ """The regions nearest to you, and the first that sells the size."""
56
+ from . import vm
57
+
58
+ vm.region(size=size, origin=origin, subscription_name=subscription)
59
+
60
+
61
+ @azure.task(name="vm", dangerous=True)
62
+ def create(
63
+ name: Annotated[str, arg(help="the VM's name; its resource group is NAME-rg")],
64
+ *,
65
+ size: Size = "Standard_B2ats_v2",
66
+ origin: Origin = None,
67
+ location: Annotated[str | None, arg(help="skip the search and use this region")] = None,
68
+ duckdns: Annotated[
69
+ str | None, arg(help="point SUBDOMAIN.duckdns.org at it (needs DUCKDNS_TOKEN)")
70
+ ] = None,
71
+ user: Annotated[str, arg(help="the login user on the VM")] = "azureuser",
72
+ no_public_ip: Annotated[bool, arg(help="private network only; avoids the billed IPv4")] = False,
73
+ paid_ok: Annotated[bool, arg(help="allow a subscription without free services")] = False,
74
+ subscription: Subscription = "",
75
+ ) -> None:
76
+ """Create a free-tier Ubuntu VM in the nearest region that will build it."""
77
+ from . import vm
78
+
79
+ vm.create(
80
+ name,
81
+ size=size,
82
+ origin=origin,
83
+ location=location,
84
+ duckdns=duckdns,
85
+ user=user,
86
+ paid_ok=paid_ok,
87
+ public_ip=not no_public_ip,
88
+ subscription_name=subscription,
89
+ )
90
+
91
+
92
+ @azure.task(name="vms")
93
+ def vms(*, subscription: Subscription = "") -> None:
94
+ """The subscription's VMs: region, size, power state, address."""
95
+ from . import vm
96
+
97
+ vm.vms(subscription_name=subscription)
98
+
99
+
100
+ @azure.task(name="dns")
101
+ def dns(name: str, subdomain: str, *, subscription: Subscription = "") -> None:
102
+ """Point SUBDOMAIN.duckdns.org at an existing VM (needs DUCKDNS_TOKEN)."""
103
+ from . import vm
104
+
105
+ vm.dns(name, subdomain, subscription_name=subscription)
106
+
107
+
108
+ @azure.task(name="delete", dangerous=True)
109
+ def delete(name: str, *, subscription: Subscription = "") -> None:
110
+ """Delete a VM's resource group: the VM, its disk, network and IP, all at once."""
111
+ from . import vm
112
+
113
+ vm.delete(name, subscription_name=subscription)