Typed, Pydantic-backed wrappers and Pythonic convenience methods for the Azure DevOps REST API
Project description
pyado — Python bindings for the Azure DevOps REST API
Typed, Pydantic-backed wrappers and Pythonic convenience methods for the Azure DevOps REST API — no raw dicts, no string parsing, full IDE completion.
pyado wraps the Azure DevOps REST API at two levels so you can pick the right abstraction for the job.
The raw layer (pyado.raw) is a thin, one-function-per-endpoint mapping of
the ADO REST surface. Every function accepts an ApiCall credential object and
one or more fully-typed Pydantic request models, then returns a fully-typed
Pydantic response model. Bad inputs are caught at model construction time, before
any HTTP request is ever issued. Pagination is transparent: list endpoints return
plain Python generators that fetch the next page automatically when the iterator
advances.
The OOP layer (pyado.oop, also re-exported at the top-level pyado)
builds on top of the raw layer and exposes every ADO resource as a Python object.
AzureDevOpsService is the single entry point; from there you navigate a strict
ownership hierarchy down to repositories, pull requests, work items, builds,
pipelines, variable groups, teams, and classification nodes. Objects cache their
data lazily on first access and share identity across paths — fetching
build.project and calling org.get_project("MyProject") both return the exact
same Project instance. Authentication, retries, connection pooling, and
content-type negotiation are handled entirely by the framework.
OOP interface
Navigate a hierarchy of resource objects instead of threading ApiCall
arguments through every call. Import via from pyado.oop import AzureDevOpsService, or use pyado.AzureDevOpsService directly.
import pyado
from pyado.raw import WorkItemFieldName
# Construct once — org URL and PAT also resolve from env vars
# (AZURE_DEVOPS_ORG / SYSTEM_TEAMFOUNDATIONCOLLECTIONURI, AZURE_DEVOPS_EXT_PAT)
svc = pyado.AzureDevOpsService(org="https://dev.azure.com/myorg", pat="<pat>")
proj = svc.org.get_project("MyProject")
# Repositories and file pushes — no local git required
repo = proj.get_repository("myrepo")
print(repo.default_branch) # "refs/heads/main"
repo.commit("main", "chore: update config", [
pyado.EditFile("/config.json", '{"key": "value"}'),
pyado.DeleteFile("/old_config.json"),
])
# Pull requests — branch names normalised automatically
pr = repo.create_pr(
title="Deploy v2.1",
source_branch="feature/v2",
target_branch="main",
description="Promotes the v2 feature branch to main.",
)
pr.add_reviewer(reviewer_id, is_required=True)
pr.add_label("ready-to-merge")
pr.link_work_item(wi) # artifact link on the work item + PR page
# Work items — full CRUD
wi = proj.get_work_item(153)
print(wi.title, wi.state) # "Fix the bug" "Active"
wi.update({WorkItemFieldName.STATE: "Resolved"})
wi.add_tag("reviewed")
wi.add_comment("Confirmed in staging — closing.", comment_format="markdown")
# Builds and pipelines
build = proj.start_build(42, source_branch="refs/heads/main")
print(build.status, build.number)
for stage in build.iter_stages():
print(stage.name, stage.result)
for job in stage.iter_jobs():
for task in job.iter_tasks():
print(f" {task.name}: {task.result}")
pipeline = proj.get_pipeline(99)
run = pipeline.start_run(template_parameters={"env": "staging"})
# Variable groups — secret-safe read-modify-write
vg = proj.get_variable_group("my-secrets")
vg.set_variable("KEY", "v2")
vg.set_variable("TOKEN", "abc123", is_secret=True)
See the full OOP usage guide for all classes and methods.
Authentication
pyado resolves credentials from three sources, checked in order:
| Source | Argument | Environment variable |
|---|---|---|
| Personal access token | pat="<token>" |
AZURE_DEVOPS_EXT_PAT |
| Organisation URL | org="https://dev.azure.com/myorg" |
AZURE_DEVOPS_ORG or SYSTEM_TEAMFOUNDATIONCOLLECTIONURI |
| Azure identity | credential=DefaultAzureCredential() |
(any azure-identity flow) |
# From environment variables — useful in CI/CD
svc = pyado.AzureDevOpsService()
# Azure managed identity or workload identity federation
from azure.identity import DefaultAzureCredential
svc = pyado.AzureDevOpsService(
org="https://dev.azure.com/myorg",
credential=DefaultAzureCredential(),
)
# Inject a custom SSL session (corporate CAs, proxies, etc.)
import requests
session = requests.Session()
session.verify = "/etc/ssl/certs/corporate-ca.pem"
svc = pyado.AzureDevOpsService(
org="https://dev.azure.com/myorg",
pat="<pat>",
session=session,
)
The underlying requests.Session is LRU-cached per access token, so
constructing multiple ApiCall objects with the same token all share
a single connection pool — no reconnect overhead.
Raw API
For scripts or advanced use-cases that need direct endpoint access, import
from pyado.raw:
from pyado.raw import (
ApiCall,
WorkItemFieldName,
get_repository_api_call,
post_wiql,
get_work_item_api_call,
get_work_item,
iter_refs,
make_ref_update,
post_push,
GitPushRequest,
GitPushCommit,
GitPushChange,
GitPushNewContent,
GitPushContentType,
post_pull_request,
PullRequestCreateRequest,
)
# A project-level ApiCall is the root credential object.
# Derive more scoped calls from it via get_*_api_call helpers.
api = ApiCall(
access_token="<your-pat>",
url="https://dev.azure.com/<organisation>/<project>/_apis/",
)
# Query work items with WIQL
refs = post_wiql(api, "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'")
for ref in refs:
item = get_work_item(get_work_item_api_call(api, ref.id))
print(f"#{item.id} {item.fields[WorkItemFieldName.TITLE]}")
# Push a file change programmatically — no local git clone required
repo_api = get_repository_api_call(api, repo_id)
current_sha = next(r.object_id for r in iter_refs(repo_api, name_filter="heads/main"))
result = post_push(
repo_api,
GitPushRequest(
ref_updates=[make_ref_update("main", current_sha)],
commits=[GitPushCommit(
comment="chore: update config",
changes=[GitPushChange(
change_type="edit",
item={"path": "/config/settings.json"},
new_content=GitPushNewContent(
content='{"key": "value"}',
content_type=GitPushContentType.raw_text,
),
)],
)],
),
)
print(f"Pushed commit {result.commits[0].commit_id}")
# Create a PR — branch names must be full refs at the raw layer
pr = post_pull_request(
repo_api,
PullRequestCreateRequest(
title="Update config",
source_ref_name="refs/heads/feature/update-config",
target_ref_name="refs/heads/main",
),
)
print(f"PR #{pr.pull_request_id} created")
What you get
-
Full type safety. Every function accepts and returns Pydantic models. Bad inputs — wrong URL scheme, missing required field, invalid UUID — are caught at construction time with a clear validation error, not buried inside an HTTP 400 response long after the call was made. IDE completion works on every field of every request and response model.
-
No boilerplate. Authentication (Basic Auth with PAT or Azure identity), session management (LRU-cached connection pools keyed on the token), automatic retries on transient connection resets, and content-type negotiation (JSON vs JSON Patch vs octet-stream) are handled transparently. Every call site looks the same.
-
Automatic pagination. Every list endpoint returns a plain Python generator. Page boundaries,
$skip/$topbookkeeping, and the ADO diff endpoint'sallChangesIncludedstop flag are managed internally. Write aforloop, get all items. -
Optimistic concurrency for git operations. pyado reads the current HEAD SHA before every push and passes it as
old_object_id, so concurrent pushes to the same branch cannot silently overwrite each other — ADO rejects the later write, and the caller retries with the updated SHA.ZERO_SHAmarks branch creation (reject if already exists) and branch deletion (set HEAD to null). -
Pythonic convenience methods. The OOP layer wraps multi-step ADO workflows behind clean, intent-expressing methods. Push a commit without touching git internals. Create a PR and attach work items in two lines. Fetch all log output for a build in a single call. Manage tags on work items as plain Python lists, with case-insensitive deduplication matching ADO's own normalisation.
-
Shared object identity. The OOP service deduplicates resource objects by identity:
build.project is wi.projectis guaranteed when both objects belong to the same project, regardless of how they were fetched. Back navigation (.project,.repo,.org) is always zero-cost. -
Everything covered. Work items (full CRUD, WIQL queries, comments, attachments, tags, relations, artifact links), pull requests (lifecycle, reviewers, threads, status checks, labels, iterations, auto-complete), git repositories (push, refs, branches, diffs, commits, ACLs), builds (queue, cancel, retry, stages/jobs/tasks, logs, artifacts, tags), pipelines (YAML runs, template parameters, resource permissions, approvals), variable groups (read, write, secrets, create, delete), teams (members, sprint iterations, area paths), classification nodes (iterations, areas), and user profiles.
-
Pipeline task callback support. pyado exposes the full distributed task plane API used inside Azure Pipelines agent jobs: write to the task feed and task log in real time, update timeline record state, and signal completion from an external process or serverless function.
Installation
$ pip install pyado
or with uv:
$ uv add pyado
Requires Python 3.11 and an Azure DevOps personal access token (PAT).
The azure-identity package is optional; install it only if you need
managed identity or federated workload authentication:
$ pip install pyado[azure-identity]
Is pyado right for you?
Microsoft also publishes an official azure-devops Python
package. See the alternatives comparison for a side-by-side
overview to help you decide which package fits your use case.
Further reading
- OOP interface guide — all OOP classes and methods with examples
- Full usage guide — every domain with detailed examples
- API reference — auto-generated from docstrings
- Contributor Guide — coding standards, architecture, and how to get started
- Alternatives — side-by-side comparison with
azure-devopsand rawrequests
Contributing
Contributions are very welcome. See the Contributor Guide for details.
License
Distributed under the terms of the MIT license. pyado is free and open source software.
Issues
Please file an issue with a detailed description of the problem.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyado-0.6.0.tar.gz.
File metadata
- Download URL: pyado-0.6.0.tar.gz
- Upload date:
- Size: 149.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7511f279be5e17ac0d8d678ae2c8018b29888f2973ad81b969a4e02cd4819245
|
|
| MD5 |
721e578a288bd33bc9182f6fddba5c67
|
|
| BLAKE2b-256 |
6fe657058ef15ea380ca2cf3bfd95454792cfabbc7f97c0658c3021a25db3d4e
|
Provenance
The following attestation bundles were made for pyado-0.6.0.tar.gz:
Publisher:
release.yml on FredStober/pyado
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyado-0.6.0.tar.gz -
Subject digest:
7511f279be5e17ac0d8d678ae2c8018b29888f2973ad81b969a4e02cd4819245 - Sigstore transparency entry: 1753875263
- Sigstore integration time:
-
Permalink:
FredStober/pyado@144a8c3b5ddd8ad87bfa40f835a80e8d5e4d89cb -
Branch / Tag:
refs/heads/main - Owner: https://github.com/FredStober
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@144a8c3b5ddd8ad87bfa40f835a80e8d5e4d89cb -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyado-0.6.0-py3-none-any.whl.
File metadata
- Download URL: pyado-0.6.0-py3-none-any.whl
- Upload date:
- Size: 198.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9db9e9c09499618e2c249a905f2bc4022abc8a21c1e943fcbd2c36e18aea5670
|
|
| MD5 |
522be26f4168bd54ec3eec3f8608371d
|
|
| BLAKE2b-256 |
e72bde98c7f71fb4c5575dfe6f5e4902f12fe5a069c1844b61501530eec1d2b3
|
Provenance
The following attestation bundles were made for pyado-0.6.0-py3-none-any.whl:
Publisher:
release.yml on FredStober/pyado
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyado-0.6.0-py3-none-any.whl -
Subject digest:
9db9e9c09499618e2c249a905f2bc4022abc8a21c1e943fcbd2c36e18aea5670 - Sigstore transparency entry: 1753875309
- Sigstore integration time:
-
Permalink:
FredStober/pyado@144a8c3b5ddd8ad87bfa40f835a80e8d5e4d89cb -
Branch / Tag:
refs/heads/main - Owner: https://github.com/FredStober
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@144a8c3b5ddd8ad87bfa40f835a80e8d5e4d89cb -
Trigger Event:
push
-
Statement type: