az-artifacts
Native Python downloads for Azure DevOps Universal Packages, without ArtifactTool or an Azure CLI runtime dependency.
Status: early, download-only implementation (0.1.0), starting issue #1. This implements a private transfer protocol using the Rust implementation as reference. Mock-based tests are not proof of service compatibility: live Azure DevOps interoperability has not yet been verified. Treat this as experimental, not a production-ready replacement for Microsoft's tooling.
Universal Package publishing/uploading is not supported. There is no command-line entry point, subprocess wrapper, or fallback to ArtifactTool. Azure DevOps Server (on-premises) is also unsupported; the client targets Azure DevOps Services.
Installation
Requires Python 3.11 or newer. The runtime dependencies are httpx and wcmatch.
Install the package from PyPI:
uv add az-artifacts
To work from this checkout:
uv sync --locked --group dev
uv run --locked python
To use the checkout from another uv project:
uv add /path/to/az-artifacts
Download a package
Provide a Personal Access Token (PAT) with Packaging: Read and access to the feed. Inject it through your shell's secret handling or CI secret store; do not put it in source code, URLs, or command-line arguments.
import os
from az_artifacts import UniversalPackageClient
pat = os.environ["AZURE_DEVOPS_EXT_PAT"]
with UniversalPackageClient("https://dev.azure.com/org", credential=pat) as client:
result = client.download(
feed="feed",
name="package",
version="1.2.3",
path="./download",
scope="project",
project="project",
file_filter="**/*.txt",
overwrite=True,
)
print(result.metadata.version)
print(result.path)
print(result.files)
print(result.bytes_downloaded)
Reading AZURE_DEVOPS_EXT_PAT above is explicit application code: the library
does not read that variable, Azure CLI credentials/configuration, or Git remotes
automatically.
Authentication options
-
A plain
credentialstring is always a PAT, sent using HTTP Basic authentication. -
Wrap an already-acquired OAuth access token in
BearerTokento send it as a bearer token, rather than accidentally treating it as a PAT:import os from az_artifacts import BearerToken, UniversalPackageClient with UniversalPackageClient( "https://dev.azure.com/org", credential=BearerToken(os.environ["SYSTEM_ACCESSTOKEN"]), ) as client: result = client.download(feed="feed", name="package", version="1.2.3", path="./download")
In Azure Pipelines, explicitly map
System.AccessTokeninto that environment variable and grant the pipeline identity access to the feed. -
Optionally install the
azure-identityextra and pass a synchronousTokenCredential, such asDefaultAzureCredential:uv sync --locked --group dev --extra azure-identity
from azure.identity import DefaultAzureCredential from az_artifacts import UniversalPackageClient with DefaultAzureCredential( exclude_cli_credential=True, exclude_developer_cli_credential=True, exclude_powershell_credential=True, ) as credential: with UniversalPackageClient("https://dev.azure.com/org", credential=credential) as client: result = client.download(feed="feed", name="package", version="1.2.3", path="./download")
Configure an appropriate environment, workload, or managed identity with Azure DevOps/feed access. The example excludes CLI-based credential providers so it does not depend on installed command-line tools. The library requests the Azure DevOps scope
499b84ac-1321-427f-aa17-267ca6975798/.default. The caller owns the credential's lifetime; closing the client does not close a supplied credential.
Client and download options
Use the client as a context manager, or call close() when finished.
The public discover_services() method returns the discovered service
name-to-URL mapping and caches the resource-area lookup for the client's
lifetime. Downloads perform this discovery automatically.
| Client argument | Default / meaning |
|---|---|
organization |
Required organization name, https://dev.azure.com/org, or legacy https://org.visualstudio.com URL. No automatic organization detection. |
credential |
Required explicit PAT, BearerToken, or synchronous TokenCredential. |
timeout |
60.0 seconds; positive, finite HTTP timeout. |
retries |
3; bounded retries for transient request failures. Use 0 to disable retries. |
max_workers |
4; bounds concurrent file download workers. |
max_manifest_bytes |
64 * 1024 * 1024; limits the decoded manifest size. |
transport |
None; optional httpx.BaseTransport, such as httpx.MockTransport for tests. |
All arguments to download() are keyword-only:
| Argument | Default / meaning |
|---|---|
feed |
Required feed name or ID. |
name |
Required Universal Package name. |
version |
Required exact version or supported wildcard selector. |
path |
Required destination as a string or pathlib.Path. |
scope |
"organization" by default; also accepts "project". |
project |
Required project name or ID for scope="project"; omit for organization scope. |
file_filter |
None downloads every file; otherwise a glob string or sequence of patterns. |
overwrite |
True replaces existing files only after each replacement is complete; False raises FileExistsError on an existing destination file. |
Project-scoped and organization-scoped feeds are distinct. The project cannot be
inferred from the feed name; pass both scope="project" and project=... when
needed. Passing project with the default scope="organization" raises
ValueError.
Version selection and file filters
Exact versions, including prereleases such as 1.2.3-rc.1, are accepted.
Wildcard selectors *, 1.*, and 1.2.* resolve to the latest stable matching
numeric SemVer, not lexical order (1.10.0 is newer than 1.9.0). Wildcards do not
select prereleases; use an exact prerelease version instead.
Filters match package-relative POSIX paths, using / even on Windows.
Supported forms include *, ?, character classes ([ab]), recursive **,
extended globs (@(src|docs)/**/*.txt), and brace expansion.
filters = ["**/*", "!**/*.tmp", "!private/**"]
A sequence is evaluated in order: matching include patterns select a file and
matching !-prefixed exclude patterns remove it. A later include can select it
again. Start with an include such as "**/*" when excluding from the entire
package; an exclusion by itself does not imply "include everything." The extended
glob form !(...) is not interpreted as an exclusion prefix. Matching is
case-sensitive and includes dotfiles. A supplied filter matching no files raises
NoMatchingFilesError. Full Azure CLI/ArtifactTool glob parity is not claimed.
Results and failure behavior
download() returns a DownloadResult:
| Field | Meaning |
|---|---|
metadata.version |
Resolved exact package version. |
metadata.manifest_id |
Package manifest identifier. |
metadata.super_root_id |
Package super-root identifier. |
metadata.package_size |
Advertised whole-package size, before filtering. |
path |
Resolved destination pathlib.Path. |
files |
Tuple of relative pathlib.Path objects for downloaded files; combine with result.path to locate them. |
bytes_downloaded |
Logical file bytes written for the selected files, not network bytes, compressed transfer size, or bytes spent on metadata/retries. |
Downloads validate content hashes and sizes, bound manifest/decompression work,
use bounded workers and retries, and reject unsafe manifest/destination paths.
The decoder distinguishes raw chunks from the supported LZ77-compressed form
using the expected SHA-512 content hash truncated to 256 bits, and enforces both
hash and size checks. Typed recursive deduplication nodes and chunked manifests
are supported. The node wire format limits each content chunk to 16 MiB minus
1 byte; tree depth is limited to 64. Decoded manifests default to a
64 MiB limit, configurable through max_manifest_bytes.
Authenticated service requests and signed blob downloads are separate: Azure DevOps credentials are not sent with signed blob URL downloads.
Files are finalized atomically one file at a time, after validation.
The default overwrite=True uses os.replace; overwrite=False uses an
exclusive hard link and therefore requires filesystem hard-link support.
Downloads restore file contents and relative paths, not original permissions,
executable bits, or symlinks.
If a file download fails, its existing destination is preserved; other files that already completed remain in place. This is not an all-or-nothing package transaction, and directories created during an unsuccessful download may remain. Do not modify the output directory concurrently, including from another download client or process.
Library errors derive from ArtifactsError, including AuthenticationError,
PermissionDeniedError, NotFoundError, VersionNotFoundError,
NoMatchingFilesError, TransportError, ProtocolError, IntegrityError, and
UnsafePathError. Invalid arguments can raise ValueError/TypeError, and local
filesystem failures can raise ordinary OSError subclasses.
Azure CLI comparison and unsupported behavior
This is a Python API, not a full reimplementation of
az artifacts universal download.
The Azure CLI command can detect organization/project context from Git
configuration and use defaults from az devops configure -d organization=... project=....
This library deliberately does neither. Pass organization, credentials, scope,
and project explicitly.
The complete download-command flag mapping is:
| Azure CLI flag | Python equivalent / limitation |
|---|---|
--feed |
download(feed=...) |
--name, -n |
download(name=...) |
--path |
download(path=...) |
--version, -v |
download(version=...) |
--org, --organization |
First UniversalPackageClient(...) argument |
--project, -p |
download(project=...), with project scope |
--scope {organization,project} |
download(scope=...), default "organization" |
--file-filter |
download(file_filter=...) |
--detect {false,true} |
Unsupported; no CLI/Git autodetection |
--acquire-policy-token, --change-reference |
Azure CLI global policy integration; unsupported |
--debug, --verbose, --only-show-errors |
No CLI logging/output flags |
--output, -o, --query |
No CLI formatting or JMESPath layer; inspect the typed result in Python |
--help, -h |
No command-line entry point; use this documentation and Python docstrings |
--subscription |
No Azure subscription selection; configure your identity and organization explicitly |
There is no publish() method, upload CLI, --description publishing option,
Azure CLI login/configuration integration, or Azure DevOps Server support.
Development and verification
uv sync --locked --group dev
uv run --locked pytest
uv run --locked ruff check .
uv run --locked ruff format --check .
uv run --locked mypy
uv build
Normal tests use local fixtures and mocked HTTP, without live credentials.
CI targets Python 3.11-3.14 on Linux and Python 3.14
on Windows/macOS. Lint, formatting, type checking, and distribution building run
once, on Linux/Python 3.14. CI builds a wheel and source distribution, installs
each into a separate clean environment, and checks imports without relying on
the source checkout. Actions are pinned to reviewed commits; ghr v0.8.0 installs
uv 0.12.15 with release verification enabled.
Optional live download smoke check
Live service verification is opt-in, against your own existing package using your own authorized read credentials. It is not run by normal tests or CI. No package/feed is created and nothing is published.
In a trusted shell, provide AZURE_DEVOPS_EXT_PAT, AZ_ARTIFACTS_ORGANIZATION,
AZ_ARTIFACTS_FEED, AZ_ARTIFACTS_PACKAGE, and an exact
AZ_ARTIFACTS_VERSION. Optionally provide AZ_ARTIFACTS_PROJECT for a
project-scoped feed. Use a new, empty ./live-download destination:
AZ_ARTIFACTS_LIVE_SMOKE=1 uv run --locked python - <<'PY'
import os
from az_artifacts import UniversalPackageClient
if os.environ.get("AZ_ARTIFACTS_LIVE_SMOKE") != "1":
raise SystemExit("Explicit live-download opt-in is required")
project = os.environ.get("AZ_ARTIFACTS_PROJECT") or None
with UniversalPackageClient(
os.environ["AZ_ARTIFACTS_ORGANIZATION"],
credential=os.environ["AZURE_DEVOPS_EXT_PAT"],
) as client:
result = client.download(
feed=os.environ["AZ_ARTIFACTS_FEED"],
name=os.environ["AZ_ARTIFACTS_PACKAGE"],
version=os.environ["AZ_ARTIFACTS_VERSION"],
path="./live-download",
scope="project" if project else "organization",
project=project,
overwrite=False,
)
print(result.metadata.version, len(result.files), result.bytes_downloaded)
PY
Compare the downloaded files with your known package contents. A successful mock test or build does not substitute for this interoperability check.
Releasing the Python distribution to PyPI
The release workflow runs only on pushed v* tags.
It checks out the exact triggering commit, reads the static version from
pyproject.toml with tomllib, and requires an exact match such as
v0.1.0 -> 0.1.0. It runs tests, lint, formatting, and type checking before
building and smoke-installing the wheel and source distribution. A separate
publish job downloads those build artifacts and uses OIDC Trusted Publishing
inside the pypi environment. There are no PyPI password/API-token secrets.
Maintainer setup is required before the first release:
- Verify the
az-artifactsPyPI name is available and configure a PyPI project or pending Trusted Publisher. - Configure the GitHub Trusted Publisher on PyPI with owner
cataggar, repositoryaz-artifacts, workflow filenamepypi.yml, and environmentpypi. Adjust owner/repository if maintaining a fork. - Create the GitHub
pypienvironment and restrict deployment to release tags (v*). This repository publishes automatically for matching tags, without required reviewers. Add reviewer approval if your release policy requires it. Merely naming an environment in YAML does not configure these protection rules. Protect release-tag creation through repository rules as appropriate. - Commit the intended version, lockfile, and tested changes, then create/push
the matching
v<version>tag when ready to release. If required reviewers are configured, approve the publish job only after reviewing its source and build results. Build artifacts are retained for 14 days, so any required approvals must occur before they expire.
PyPI publishing here distributes the Python library; it does not add Universal Package publishing support.
License and provenance
MIT licensed. The native download implementation is based on the Microsoft
MIT-licensed Rust code in
../azure-devops-rust-api/azure_devops_rust_api/src/artifacts_download, including
its Universal Package metadata, deduplication, and decompression protocol work.
The typed node format and content hashes also follow Microsoft's
BuildXL hashing implementation.
The exact upstream Microsoft license and copyright notice are retained in the
root LICENSE; preserve that notice when redistributing derived code.
This Python implementation is not an official Microsoft SDK.
Release files for az-artifacts 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| az_artifacts-0.1.0.tar.gz | 21.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| az_artifacts-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 47.5 kB
Release files / az_artifacts-0.1.0.tar.gz
| Download URL | az_artifacts-0.1.0.tar.gz |
|---|---|
| Size | 21.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
41edd3444add295357796a7c8cb31ab007889c959ed255c0801c244178bf91be
|
|
BLAKE2b-256 checksum How to use checksums |
2cce6c42b2cddb30414aa20ac684232f48d96c6d549d5978d12db62bcbce08dc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency logRelease files / az_artifacts-0.1.0-py3-none-any.whl
| Download URL | az_artifacts-0.1.0-py3-none-any.whl |
|---|---|
| Size | 26.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4d0c43bd01c85a9dbb9f2c9d8f6c67fc10a9d378e82b8e78fb8a17e8bdd13b32
|
|
BLAKE2b-256 checksum How to use checksums |
dc81d70f4a9a9c5c4f2d92b6a5c46157da525858b477f1f3143e7b514a0209db
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency log