Skip to main content

Add your description here

Project description

pyado — Pythonic Azure DevOps Interface

PyPI Status Python Version License

Read the documentation at https://pyado.readthedocs.io/ Tests Ruff

Typed Python wrapper around the Azure DevOps REST API, built on Pydantic models. All functions accept an ApiCall object and return typed results — no raw dicts, no string parsing.


Requirements

Installation

$ pip install pyado

or with uv:

$ uv add pyado

Quick Start

Every function takes an ApiCall as its first argument. Construct one with your organisation/project base URL and a PAT:

import pyado

# Project-level API call (most functions need this)
api = pyado.ApiCall(
    access_token="<your-pat>",
    url="https://dev.azure.com/<organisation>/<project>/_apis/",
)

# Organisation-level API call (iter_projects, iter_open_prs across projects)
org_api = pyado.ApiCall(
    access_token="<your-pat>",
    url="https://dev.azure.com/<organisation>/_apis/",
)

# Profile API call (get_my_profile)
profile_api = pyado.ApiCall(
    access_token="<your-pat>",
    url="https://app.vssps.visualstudio.com/_apis/",
)

ApiCall is a Pydantic BaseModel — it validates inputs on construction and is immutable. Use build_call() to derive a scoped call pointing at a specific resource.

All public symbols are available directly from the pyado namespace.


Work items

import pyado

# Fetch multiple work items (batched automatically, 200 per request)
for item in pyado.iter_work_item_details(api, [123, 456]):
    print(item.id, item.fields["System.Title"])

# Fetch a single work item with relations
work_item_api = pyado.get_work_item_api_call(api, 123)
item = pyado.get_work_item(work_item_api, expand_relations=True)

# Create a work item with a parent link
new_item = pyado.create_work_item(
    api,
    fields={
        "System.WorkItemType": "Task",
        "System.Title": "My task",
        "System.AreaPath": "MyProject\\Team",
    },
    relations=[
        pyado.WorkItemRelation(
            rel="System.LinkTypes.Hierarchy-Reverse",
            url="https://dev.azure.com/org/project/_workitems/edit/100",
        )
    ],
)

# Update fields (markdown description example)
pyado.update_work_item(
    work_item_api,
    fields={"System.Description": "## Summary\nSome details."},
    multiline_fields_format={"System.Description": "markdown"},
)

# WIQL query
refs = pyado.post_wiql(api, "SELECT [System.Id] FROM WorkItems WHERE [System.State] = 'Active'")
ids = [ref.id for ref in refs]

# Comments
for comment in pyado.iter_work_item_comments(work_item_api):
    print(comment.text)

pyado.post_work_item_comment(work_item_api, "Reviewed and confirmed.", comment_format="markdown")

# Attach a file
pyado.add_work_item_attachment(api, 123, "report.txt", b"file contents here")

# Sprint iterations
for sprint in pyado.iter_sprint_iterations(api):
    print(sprint.name, sprint.attributes.start_date)

for sprint in pyado.iter_sprint_iterations(api, timeframe_filter="current"):
    print(sprint.name)

Pull requests

import uuid
import pyado

repo_id: pyado.RepositoryId = uuid.UUID("<repository-uuid>")
repo_api = pyado.get_repository_api_call(api, repo_id)
pr_api = pyado.get_pr_api_call(api, repo_id, pr_id=42)

# List all active PRs in the project
for pr in pyado.iter_open_prs(api):
    print(pr.pr_id, pr.repository.id)

# List PRs matching criteria
for pr in pyado.iter_prs(api, {"status": "active", "creatorId": "<identity-id>"}):
    print(pr.pr_id)

# Create a PR
new_pr = pyado.create_pr(
    repo_api,
    title="My feature",
    source_branch="feature/my-branch",
    target_branch="main",
    description="Details here.",
)

# Update PR fields
pyado.patch_pr(pr_api, pyado.PullRequestUpdateRequest(title="Updated title"))

# Work items linked to the PR
for work_item_id in pyado.iter_pr_work_item_ids(pr_api):
    print(work_item_id)

# Labels
labels = pyado.get_pr_labels(pr_api)
pyado.post_pr_label(pr_api, "ready-to-merge")
pyado.delete_pr_label(pr_api, "needs-review")

# Review threads
for thread in pyado.iter_pr_threads(pr_api):
    for comment in thread.comments:
        print(comment.content)

thread = pyado.create_pr_thread(
    pr_api,
    "Please address this.",
    file_path="/src/foo.py",
    line=42,
)
pyado.reply_to_pr_thread(pr_api, thread.id, "Done, thanks.")

# Iterations (commit pushes to the PR source branch)
for iteration in pyado.iter_pr_iterations(pr_api):
    print(iteration.id, iteration.source_ref_commit)

# Reviewer management
pyado.set_pr_reviewer_vote(pr_api, "<reviewer-identity-id>", pyado.PullRequestVote.APPROVED)
pyado.add_pr_reviewer(pr_api, "<reviewer-identity-id>", is_required=True)
pyado.delete_pr_reviewer(pr_api, "<reviewer-identity-id>")

# Post a status flag (e.g. from a CI check)
pyado.post_pr_status(
    pr_api,
    pyado.PullRequestStatusRequest(
        context=pyado.PullRequestStatusContext(genre="ci", name="build"),
        description="Build passed",
        iteration_id=1,
        state="succeeded",
    ),
)

Repository

import uuid
import pyado

repo_id = uuid.UUID("<repository-uuid>")
repo_api = pyado.get_repository_api_call(api, repo_id)

# List repositories
for repo in pyado.iter_repository_details(api):
    print(repo.name, repo.id)

# Read a file at a specific commit
content = pyado.get_file_content_at_commit(repo_api, "/src/config.json", "abc123")

# Read a file at a branch tip
content = pyado.get_file_content_at_branch(repo_api, "/src/config.json", "main")

# File changes between two commits (paginated)
for change in pyado.iter_commit_diff(repo_api, base_commit="abc123", target_commit="def456"):
    print(change.change_type, change.item.path)

# Most recent commit touching a file
commit_sha = pyado.get_last_commit_touching_file(repo_api, "/src/foo.py", before_commit="def456")

# Refs (branches / tags)
for ref in pyado.iter_refs(repo_api, name_filter="heads/main"):
    print(ref.name, ref.object_id)

# Branch management
pyado.create_branch(repo_api, "feature/new-branch", from_commit="abc123")
pyado.delete_branch(repo_api, "feature/old-branch", current_commit="abc123")

Git push

import pyado

repo_api = pyado.get_repository_api_call(api, repo_id)

# Push one or more file changes in a single commit
result = pyado.push_commits(
    repo_api,
    ref_updates=[pyado.make_ref_update("main", "abc123")],
    commits=[
        pyado.make_commit("Update settings", [
            pyado.add_file("/config/new.json", '{"created": true}'),
            pyado.edit_file("/config/settings.json", '{"key": "value"}'),
            pyado.delete_file("/config/old.json"),
            pyado.rename_file("/config/a.json", "/config/b.json"),
        ])
    ],
)
print(result.push_id, result.commits[0].commit_id)

Build

import pyado

build_api = pyado.get_build_api_call(api, build_id=1234)

# Top-level build details
details = pyado.get_build_details(build_api)
print(details.status, details.result, details.source_branch)

# List recent builds
for build in pyado.iter_builds(api, definition_id=42, status_filter="inProgress"):
    print(build.id, build.build_number)

# Queue a new build
queued = pyado.start_build(
    api,
    definition_id=42,
    source_branch="refs/heads/main",
    parameters={"env": "staging"},
)

# Timeline records (stages, jobs, tasks)
for record in pyado.iter_timeline_records(build_api):
    print(record.type_name, record.name, record.state, record.result)

# Work items linked to a build
for work_item_id in pyado.iter_build_work_item_ids(build_api):
    print(work_item_id)

# Work items introduced between two build IDs
for ref in pyado.iter_work_items_between_builds(api, from_build_id=100, to_build_id=200):
    print(ref.id)

# Build artifacts
for artifact in pyado.iter_build_artifacts(build_api):
    print(artifact.name, artifact.resource.download_url)

# Build tags
for tag in pyado.iter_build_tags(build_api):
    print(tag)
pyado.post_build_tag(build_api, "release-candidate")
pyado.delete_build_tag(build_api, "release-candidate")

# Pipeline definitions (classic / Build API)
for defn in pyado.iter_pipeline_definitions(api, name_filter="deploy"):
    print(defn.id, defn.name)

Pipeline (task callbacks)

Used to interact with a running pipeline task from within a task script (e.g. an agent job calling back to ADO).

import uuid
import pyado

plan_id = uuid.UUID("<plan-uuid>")
timeline_id = uuid.UUID("<timeline-uuid>")
job_id = uuid.UUID("<job-uuid>")
log_id = 1

plan_api = pyado.get_plan_api_call(api, hub_name="build", plan_id=plan_id)
job_api = pyado.get_job_api_call(api, "build", plan_id, timeline_id, job_id)
log_api = pyado.get_log_api_call(api, "build", plan_id, log_id)

# Send messages to the task feed (shown in the ADO UI)
pyado.send_job_feed(job_api, ["Step 1 complete", "Step 2 starting..."])

# Append content to the task log
pyado.post_job_logs(log_api, "Detailed log output here.\n")

# Signal task completion
pyado.send_job_event(
    plan_api,
    task_id=uuid.UUID("<task-uuid>"),
    job_id=job_id,
    job_event_name="TaskCompleted",
    job_event_result="succeeded",
)

# Pending environment approvals
for approval in pyado.iter_pending_approvals(api):
    print(approval.id, approval.status)

pyado.approve_pipeline(api, approval_id="<approval-uuid>", comment="LGTM")

Pipeline runs (YAML pipelines)

The /pipelines REST API covers YAML pipelines and their runs separately from the Build Definitions API.

import pyado

# List all YAML pipelines in the project
for pipeline in pyado.iter_pipelines(api):
    print(pipeline.id, pipeline.name, pipeline.folder)

# Fetch a single pipeline
pipeline = pyado.get_pipeline(api, pipeline_id=42)

# List runs for a pipeline (newest first)
for run in pyado.iter_pipeline_runs(api, pipeline_id=42):
    print(run.id, run.state, run.result)

# Fetch a single run
run = pyado.get_pipeline_run(api, pipeline_id=42, run_id=1)

# Trigger a new run
run = pyado.post_pipeline_run(
    api,
    pipeline_id=42,
    request=pyado.PipelineRunRequest(
        template_parameters={"env": "staging"},
    ),
)
print(run.id, run.state)

Projects

import pyado

# Requires an organisation-level ApiCall
for project in pyado.iter_projects(org_api):
    print(project.id, project.name)

Variable groups

import pyado

# List all variable groups in the project
for vg in pyado.iter_variable_group_details(api):
    print(vg.id, vg.name, vg.variables)

# Update variables in a group
vg = next(vg for vg in pyado.iter_variable_group_details(api) if vg.id == 42)
vg_api = pyado.get_variable_group_api_call(api, 42)
pyado.update_variable_group(
    vg_api,
    name=vg.name,
    variables={
        "MY_VAR": pyado.VariableInfo(value="new-value"),
        "SECRET_VAR": pyado.VariableInfo(value="secret", is_secret=True),
    },
    variable_group_project_references=vg.variable_group_project_references,
)

Profile

import pyado

# Requires the profile ApiCall (app.vssps.visualstudio.com)
me = pyado.get_my_profile(profile_api)
print(me.display_name, me.email_address)

Development

Package structure

The library is split into two subpackages that are re-exported through the top-level pyado namespace:

Subpackage Purpose
pyado.raw Thin wrappers around individual ADO REST endpoints. Each function makes exactly one API call, accepts a typed Pydantic request model, and returns a typed Pydantic response model. No payload construction, no orchestration.
pyado.high Higher-level helpers. Responsible for constructing request models from primitive args, pagination loops, and multi-step operations. Delegates all HTTP calls to pyado.raw.

Both subpackages are split into domain modules:

Module Covers
raw/_core.py ApiCall, shared types
raw/build.py Builds, timeline records, artifacts, tags, pipeline definitions
raw/git.py Repositories, commits, refs, file content, git push
raw/pipeline.py YAML pipeline runs, distributed task plane (job feed, events, approvals)
raw/profile.py User profile
raw/project.py Projects
raw/pull_request.py Pull requests, threads, reviewers, labels, statuses
raw/variable_group.py Variable groups
raw/work_item.py Work items, comments, WIQL, sprint iterations
high/build.py start_build, send_job_feed, send_job_event, approve_pipeline, etc.
high/git.py push_commits, iter_commit_diff, get_file_content_at_*, branch helpers
high/pull_request.py create_pr, create_pr_thread, reply_to_pr_thread, reviewer helpers
high/variable_group.py update_variable_group
high/work_item.py create_work_item, update_work_item, iter_work_item_details, attachment

Rules for raw/:

  • One function per ADO REST endpoint; no multi-step logic.
  • Accept fully-built Pydantic request models — no model construction inside the function.
  • Return Pydantic response models; never raw dict.
  • Multi-field request models are named publicly so callers can reference them directly.

Rules for high/:

  • Construct request models from plain Python values (strings, ints, enums).
  • Own pagination loops; yield individual items.
  • May use intent-expressing names that differ from the underlying raw function (e.g. push_commits wraps post_push, start_build wraps post_build).
  • Never call api_call.get / post / patch / ... directly — always delegate to raw/.
  • Never re-export raw symbols; every public symbol in high/ must be a function defined in that module.

Adding new functionality:

  1. Add the HTTP call (and any request/response models) to the appropriate domain submodule in raw/ (e.g. raw/git.py, raw/build.py, raw/pull_request.py).
  2. Export new public symbols from raw/__init__.py and pyado/__init__.py.
  3. If payload construction, pagination, or orchestration is needed, add a wrapper in the matching domain submodule in high/ (e.g. high/git.py), then export it from high/__init__.py and pyado/__init__.py.

Setting up a development environment

You need Python 3.11 and uv:

$ uv sync

Run the test suite:

$ uv run pytest

Run linting and type checks:

$ uv run ruff check src/
$ uv run mypy src/

Contributing

Contributions are very welcome. To learn more, see the Contributor Guide.

License

Distributed under the terms of the MIT license, pyado is free and open source software.

Issues

If you encounter any problems, please file an issue along with a detailed description.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyado-0.2.1.tar.gz (51.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyado-0.2.1-py3-none-any.whl (69.0 kB view details)

Uploaded Python 3

File details

Details for the file pyado-0.2.1.tar.gz.

File metadata

  • Download URL: pyado-0.2.1.tar.gz
  • Upload date:
  • Size: 51.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.10 {"installer":{"name":"uv","version":"0.9.10"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Amazon Linux","version":"2023","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pyado-0.2.1.tar.gz
Algorithm Hash digest
SHA256 9829c92242e8429a37789e31911fa2736a78365a42cc5181b7496d81b0c66671
MD5 c0f558b1e91a088909edd1e8fda3635a
BLAKE2b-256 c7aa99744106c4525bff1aa3ea0beead95a88f328fc54a7f954931f95e42639e

See more details on using hashes here.

File details

Details for the file pyado-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: pyado-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 69.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.10 {"installer":{"name":"uv","version":"0.9.10"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Amazon Linux","version":"2023","id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pyado-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b9d19f6db4f1d3d674bd78184bd04a8a8ee2ef6e3682515d54ba39e883090804
MD5 ddbcf4e75e8ed1ddb699314136e98c43
BLAKE2b-256 2b16e9889355adebeaf620d03d36e7afd67e6019857b7669109a37ce683b1f02

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page