SDK
Python SDK
A typed client for the Atlas REST API, generated from the OpenAPI spec. Works in any Python 3.8+ project with no third-party dependencies.
Overview
The Atlas Python SDK wraps the same REST surface documented in the API guide. One Client class exposes a method per operation, handles bearer-token auth, and raises a typed error hierarchy on non-2xx responses. It is generated from docs/atlas-openapi.json, so method names track the API exactly and never drift from the reference.
Method naming
operationId. Listing tasks (operation atlas_get_tasks) is client.atlas_get_tasks(...). The methods overview maps the common resources.Install
Published on PyPI. Pin the version to match your API tier in production.
pip install atlas-sdkpoetry add atlas-sdkAuthentication
Every request carries a Personal Access Token as a bearer credential. Tokens start with atlas_pat_ and are scope-bounded and revocable.
import os
from atlas_sdk import Client
# Read the token from the environment - never hard-code it in source.
token = os.environ["ATLAS_TOKEN"]
client = Client(token=token)Scopes are enforced server-side
403 (insufficient_scope) and raises ForbiddenError - the SDK never widens a token. Mint the narrowest scope set that works and widen only when a call is refused.Quickstart
One authenticated request, end to end. Set ATLAS_TOKEN in your environment first.
import os
from atlas_sdk import Client
client = Client(token=os.environ["ATLAS_TOKEN"])
# List endpoints answer an { "items": [...], "nextCursor": ... } envelope.
page = client.atlas_get_tasks(query={"limit": 5})
for task in page["items"]:
print(task["id"], task["title"])Core examples
The everyday task and project operations. Reads answer an { items, nextCursor } envelope; writes take a body mapping.
List tasks with filters
# Filters map straight to the REST query string. See the API reference for the
# full set; the server ignores keys it does not recognise.
page = client.atlas_get_tasks(query={
"projectId": "prj_123",
"status": "IN_PROGRESS",
"assigneeId": "usr_42",
"limit": 50,
})
tasks = page["items"]Get a task
# Single-resource routes carry the id in the path. Every typed method delegates
# to Client._request, so call it directly to fill the id in.
task = client._request("GET", f"/tasks/{task_id}")
print(task["title"], task["status"])Single-resource routes
atlas_get_tasks_record method targets /tasks/{id} but does not interpolate the id, so fill it through the shared _request transport as shown. The same applies to update and delete.Create a task
created = client.atlas_post_tasks(body={
"projectId": "prj_123",
"title": "Review Q3 forecast",
"priority": "HIGH",
"dueOn": "2026-05-01T17:00:00Z",
})
print("created", created["id"])Update a task
updated = client._request(
"PATCH",
f"/tasks/{task_id}",
body={"status": "DONE"},
)Cursor-based pagination
def iter_tasks(client, **filters):
"""Yield every task across all pages by following nextCursor."""
cursor = None
while True:
query = {**filters, "limit": 100}
if cursor:
query["cursor"] = cursor
page = client.atlas_get_tasks(query=query)
for task in page["items"]:
yield task
cursor = page.get("nextCursor")
if not cursor:
break
for task in iter_tasks(client, projectId="prj_123"):
print(task["id"])List projects
projects = client.atlas_get_projects(query={"limit": 50})
for project in projects["items"]:
print(project["id"], project["name"])Error handling
Every non-2xx response raises a subclass of ApiError. The error carries the HTTP status and the parsed problem+json body.
from atlas_sdk import (
ApiError,
UnauthorizedError,
ForbiddenError,
)
try:
client.atlas_post_tasks(body={"title": ""})
except ForbiddenError as err:
# 403: the token is missing a scope. The scopes are in the problem body.
problem = err.body or {}
print("need scopes:", problem.get("requiredScopes"))
except UnauthorizedError:
# 401: token invalid or revoked - re-authenticate.
raise
except ApiError as err:
# 409 / 422 / 429 and anything else land here with .status set.
problem = err.body or {}
print(err.status, problem.get("title"), problem.get("requestId"))
if err.status == 422:
for field in problem.get("errors", []):
print(field["path"], field["message"])
raise| HTTP status | Error class | Meaning |
|---|---|---|
| 400 | BadRequestError | Malformed request or query. |
| 401 | UnauthorizedError | Token missing, invalid, expired, or revoked. |
| 403 | ForbiddenError | Token is missing a required scope. |
| 404 | NotFoundError | Resource does not exist or is out of tenant. |
| 409, 422, 429 | ApiError (base) | No dedicated class - branch on the status code and read the body. |
| 5xx | ServerError | Transient server fault. Safe to retry. |
Only the five statuses above get a dedicated class. Conflicts (409), validation failures (422), and rate limits (429) raise the base ApiError, so branch on err.status and read the body. Every error wraps the API's RFC 9457 problem+json envelope:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
x-request-id: f47ac10b-58cc-4372-a567-0e02b2c3d479
{
"type": "https://atlas.dev/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"detail": "This Personal Access Token is missing one of the required scopes: tasks:write.",
"requiredScopes": ["tasks:write"],
"grantedScopes": ["tasks:read"],
"requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}err.body is that JSON object: type, title, status, detail, and a requestId to quote in support tickets. A 422 adds an errors array; a 429 adds a rateLimit object.
Retries and rate limits
Retry 429 and 5xx; never retry 4xx you caused. The 429 body carries the retry delay, so honour it before falling back to backoff.
import random
import time
from atlas_sdk import ApiError
RETRYABLE = {429, 500, 502, 503, 504}
def with_retries(call, *, max_attempts=5):
"""Retry on 429 and 5xx with exponential backoff and jitter."""
for attempt in range(1, max_attempts + 1):
try:
return call()
except ApiError as err:
if err.status not in RETRYABLE or attempt == max_attempts:
raise
problem = err.body or {}
rate_limit = problem.get("rateLimit") or {}
# The 429 body carries the authoritative retry delay, in seconds.
delay = rate_limit.get("retryAfterSec")
if delay is None:
delay = min(2 ** (attempt - 1), 30) + random.random()
time.sleep(delay)
page = with_retries(lambda: client.atlas_get_tasks(query={"limit": 50}))Back off with jitter
retryAfterSec is present, use exponential backoff with a random jitter so a fleet of clients does not retry in lockstep. Cap total attempts - a persistent 429 means you need a higher tier, not more retries.Idempotency
Every write accepts an Idempotency-Key header. Replaying the same key within 24h returns the original response verbatim - safe retries on flaky networks.
import json
import os
import uuid
from urllib import request as urlrequest
from atlas_sdk import Client
class IdempotentClient(Client):
"""The generated client sends a fixed header set; this thin extension adds
an Idempotency-Key to writes so a retried request replays (24h) instead of
creating a duplicate."""
def write(self, method, path, body, *, key):
req = urlrequest.Request(
f"{self._base_url}{path}",
data=json.dumps(body).encode(),
method=method.upper(),
headers={
"Authorization": f"Bearer {self._token}",
"Content-Type": "application/json",
"Accept": "application/json",
"Idempotency-Key": key,
},
)
with urlrequest.urlopen(req, timeout=self._timeout) as resp:
raw = resp.read()
return json.loads(raw) if raw else None
client = IdempotentClient(token=os.environ["ATLAS_TOKEN"])
# Generate one key per logical write and reuse it on every retry.
key = str(uuid.uuid4())
task = client.write("POST", "/tasks", {"projectId": "prj_123", "title": "Ship it"}, key=key)Reuse the key on retry
Configuration
The constructor takes the base URL and a per-request timeout. Override the base URL for staging or a self-hosted deployment.
from atlas_sdk import Client
client = Client(
token=os.environ["ATLAS_TOKEN"],
base_url="https://api.atlas.app/v1", # override for staging or self-hosted
timeout=30.0, # seconds, applied per request
)For a custom User-Agent, request id, or any other header, extend the client as in Idempotency - the transport is a single small method you can wrap.
Methods overview
The common task and project operations. The full set - one method per operation - lives in the API reference.
| Resource | REST | SDK method |
|---|---|---|
| List tasks | GET /tasks | atlas_get_tasks(query=...) |
| Create task | POST /tasks | atlas_post_tasks(body=...) |
| Get task | GET /tasks/{id} | atlas_get_tasks_record * |
| Update task | PATCH /tasks/{id} | atlas_patch_tasks_record * |
| Delete task | DELETE /tasks/{id} | atlas_delete_tasks_record * |
| List projects | GET /projects | atlas_get_projects(query=...) |
| Create project | POST /projects | atlas_post_projects(body=...) |
* Single-resource methods exist but send the literal {id} path; fill the id through _request (see Get a task).
Next steps
The SDK is a thin, faithful layer over the REST API. When you need the exact shape of a request or response, go to the source.
- REST API guideAuth, pagination, idempotency, and rate limits at the protocol level - the contract every SDK speaks./docs/api
- API referenceEvery operation, request body, and response shape - the source of truth for method arguments./docs/api/reference
- MCP for AI agentsHand the same operations to an AI agent over the Model Context Protocol./docs/mcp