Atlassian GraphQL and Rest clients for Python and Go with shared transport OpenAPI spec.
This repo avoids stale, hand-written assumptions about Atlassian GraphQL Gateway (AGG) by generating a small, query-focused set of API models from live schema introspection.
- Fetch schema (writes
graphql/schema.introspection.jsonand optionallygraphql/schema.sdl.graphql):make graphql-schema
- Generate Jira project API models from the introspection JSON:
make graphql-gen
Notes:
- SDL output is best-effort. If the Python optional dependency for GraphQL SDL printing is not installed, only the JSON file is written.
- Generation is intentionally minimal: it only emits the types needed for the Jira project listing query and its connection/edge shapes.
In addition to Jira, this library supports Compass (component catalog) and Teams (organizational structure) via AGG:
Compass provides service catalog data including components, relationships, and scorecards.
- Generate Compass models:
python python/tools/generate_compass_component_models.py(or Go equivalent) - Canonical models:
CompassComponent,CompassRelationship,CompassScorecardScore - Mappers:
python/atlassian/graph/mappers/compass_components.py,go/atlassian/graph/mappers/compass_components.go
Teams provides organizational structure including team membership and collaboration patterns.
- Generate Teams models:
python python/tools/generate_team_models.py(or Go equivalent) - Canonical models:
AtlassianTeam,AtlassianTeamMember - Mappers:
python/atlassian/graph/mappers/teams.py,go/atlassian/graph/mappers/teams.go - Note: Some Teams queries require beta headers (
X-ExperimentalApi: teams-beta)
Teamwork Graph provides cross-product collaboration analytics:
- Generate models:
python python/tools/generate_teamwork_graph_models.py - Generated API:
python/atlassian/graph/gen/teamwork_graph_api.py,go/atlassian/graph/gen/teamwork_graph_api.go
Jira Cloud publishes a Swagger/OpenAPI spec for REST v3. You can fetch it into this repo:
make jira-rest-openapi(writesopenapi/jira-rest.swagger-v3.json)- Generate minimal, analytics-focused REST models from the swagger JSON:
make jira-rest-gen(writespython/atlassian/rest/gen/jira_api.pyandgo/atlassian/rest/gen/jira_api.go)
- Global:
https://api.atlassian.com/graphql(OAuth2 bearer token) - Tenanted gateway:
https://{subdomain}.atlassian.net/gateway/api/graphql(API token via Basic auth or browser session cookies) - Custom/non-tenanted: configurable
BaseURL(may be either the base host likehttps://api.atlassian.comor the full GraphQL URL ending in/graphql) - Jira REST (OAuth2 3LO):
https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/... - Jira REST (tenanted):
https://{subdomain}.atlassian.net/rest/api/3/...
See the transport spec in openapi/atlassian.transport.openapi.yaml.
Canonical analytics schemas:
- Jira:
openapi/jira-developer-health.canonical.openapi.yaml - Compass:
openapi/compass-developer-health.canonical.openapi.yaml - Teams:
openapi/teams-developer-health.canonical.openapi.yaml
ATLASSIAN_OAUTH_ACCESS_TOKEN is an OAuth 2.0 access token, not your app’s client secret.
- Create a 3LO app in the Atlassian Developer Console and configure a redirect URI (e.g.
http://localhost:8080/callback). - Run the helper and follow the prompts:
- Python:
make oauth-login - Python (local callback server + token file):
make oauth-login-server OAUTH_LOGIN_ARGS="--output oauth_tokens.txt" - Go:
make oauth-login-go - To print cloud IDs (accessible resources):
make oauth-login OAUTH_LOGIN_ARGS="--print-accessible-resources"(or runpython python/tools/oauth_login.py --print-accessible-resourcesdirectly)
- Python:
- Include
offline_accessin your scopes to receive a refresh token, then set:ATLASSIAN_OAUTH_ACCESS_TOKEN(short-lived)ATLASSIAN_OAUTH_REFRESH_TOKEN+ATLASSIAN_CLIENT_ID+ATLASSIAN_CLIENT_SECRET(recommended; auto-refresh)
Note: AGG may return a GraphQL error with required_scopes values (e.g. jira:atlassian-external). When classification=InsufficientOAuthScopes, this is an OAuth scope requirement surfaced by AGG; if that scope isn’t obtainable via Atlassian 3LO, you’ll need to run those operations via tenanted gateway auth (Basic API token / cookies).
For Jira project listing, the OAuth scope requirement surfaced by AGG appears to be non-standard for external apps; use Jira REST GET /rest/api/3/project/search instead when running under normal 3LO scopes (read:jira-work, read:jira-user).
from atlassian import (
GraphQLClient,
OAuthBearerAuth,
OAuthRefreshTokenAuth,
BasicApiTokenAuth,
JiraRestClient,
iter_projects_with_opsgenie_linkable_teams,
list_projects_with_opsgenie_linkable_teams,
iter_projects_via_rest,
list_projects_via_rest,
)
# OAuth bearer (api.atlassian.com)
client = GraphQLClient("https://api.atlassian.com", OAuthBearerAuth(lambda: "ACCESS_TOKEN"))
resp = client.execute("query { __typename }")
# OAuth refresh token (auto-refreshes access tokens)
client = GraphQLClient(
"https://api.atlassian.com",
OAuthRefreshTokenAuth("CLIENT_ID", "CLIENT_SECRET", "REFRESH_TOKEN"),
)
# Tenanted Basic API token
client = GraphQLClient(
"https://yourteam.atlassian.net/gateway/api",
BasicApiTokenAuth("you@example.com", "API_TOKEN"),
strict=True,
)
# Experimental APIs
client.execute("query { __typename }", experimental_apis=["jiraexpression", "anotherBeta"])
# Jira projects + linkable Opsgenie teams (canonical output)
projects = list(
iter_projects_with_opsgenie_linkable_teams(
client,
cloud_id="YOUR_CLOUD_ID",
project_types=["SOFTWARE"],
page_size=50,
experimental_apis=["someExperimentalApi"],
)
)
# Convenience wrapper (builds GraphQLClient from env vars)
projects = list(list_projects_with_opsgenie_linkable_teams("YOUR_CLOUD_ID", ["SOFTWARE"]))
# Jira projects via Jira REST (OAuth-friendly; returns empty opsgenieTeams)
rest = JiraRestClient(f"https://api.atlassian.com/ex/jira/{'YOUR_CLOUD_ID'}", OAuthBearerAuth(lambda: "ACCESS_TOKEN"))
projects = list(iter_projects_via_rest(rest, cloud_id="YOUR_CLOUD_ID", project_types=["SOFTWARE"]))
# Convenience wrapper (builds JiraRestClient from env vars)
projects = list(list_projects_via_rest("YOUR_CLOUD_ID", ["SOFTWARE"]))
# Jira Agile sprints (via Jira Software REST API)
from atlassian import iter_board_sprints_via_rest
sprints = list(iter_board_sprints_via_rest(rest, board_id=10, state="active"))import (
"context"
"atlassian/atlassian"
"atlassian/atlassian/graph"
"atlassian/atlassian/rest"
)
client := graph.Client{
BaseURL: "https://api.atlassian.com",
Auth: atlassian.BearerAuth{
TokenGetter: func() (string, error) { return "ACCESS_TOKEN", nil },
},
Strict: true,
}
// OAuth refresh token (auto-refreshes access tokens)
client = graph.Client{
BaseURL: "https://api.atlassian.com",
Auth: &atlassian.OAuthRefreshTokenAuth{
ClientID: "CLIENT_ID",
ClientSecret: "CLIENT_SECRET",
RefreshToken: "REFRESH_TOKEN",
},
Strict: true,
}
result, err := client.Execute(
context.Background(),
"query { __typename }",
nil,
"",
[]string{"jiraexpression"},
1, // estimated cost (optional)
)
if err != nil {
// handle error
}
projects, err := client.ListProjectsWithOpsgenieLinkableTeams(
context.Background(),
"YOUR_CLOUD_ID",
[]string{"SOFTWARE"},
50,
)
// Jira projects via Jira REST (OAuth-friendly; returns empty opsgenieTeams)
rest := rest.JiraRESTClient{
BaseURL: "https://api.atlassian.com/ex/jira/" + "YOUR_CLOUD_ID",
Auth: atlassian.BearerAuth{
TokenGetter: func() (string, error) { return "ACCESS_TOKEN", nil },
},
}
projects, err = rest.ListProjectsViaREST(context.Background(), "YOUR_CLOUD_ID", []string{"SOFTWARE"}, 50)
// Jira Agile sprints (via Jira Software REST API)
sprints, err := rest.ListBoardSprintsViaREST(context.Background(), 10, "active", 50)- Strict mode raises/returns GraphQL operation errors when
errors[]is present. - Non-strict mode preserves partial
dataalongsideerrors. - Rate limiting: Atlassian GraphQL Gateway enforces cost-based, per-user budgets (default 10,000 points per currency per minute). When exceeded it returns HTTP 429 with a
Retry-Aftertimestamp header (e.g.,2021-05-10T11:00Z); the 429 applies to the HTTP request, not as a GraphQL error. Clients retry only on 429, honoring the timestamp andmax_wait_seconds, and surfaceRateLimitErrordetails (including unparseable headers). No retries occur on HTTP 5xx. - Optional local throttling (best-effort, off by default): clients can enable a token bucket approximating 10,000 points/minute using a per-call
estimated_cost(default 1). If insufficient local budget, the client blocks until budget refills ormax_wait_secondsis exceeded, then raises a local throttling error. This does not replace server enforcement.
- AGG uses cost-based, per-user limits (default budget 10,000 points per currency per minute). Overages return HTTP 429 with
Retry-After: {timestamp}(e.g.,2021-05-10T11:00Z); 429 is an HTTP-level response, not a GraphQL error. Do not retry on HTTP ≥ 500. - Retry only on 429. Parse
Retry-Afteras a timestamp (support ISO-8601/RFC3339 and HTTP-date variants); if parsing fails, return aRateLimitErrorthat includes the raw header. Computewait = retry_at - now; ifwait <= 0, retry immediately (counts toward attempts). Ifwaitexceedsmax_wait_seconds, surface aRateLimitErrorwith the computed wait and cap. Retry up tomax_retries_429, otherwise return aRateLimitErrorwith the attempts count and last header/reset time. - Optional local, best-effort token bucket (off by default): bucket size 10,000 points and refill rate
10000/60per second. Eachexecutetakes anestimated_cost(default 1); if tokens are insufficient, block until budget refills ormax_wait_secondsexpires, then raise a local throttling error. This only complements server enforcement. - Logging: on 429 emit a warning with attempt number, parsed reset time, computed wait, endpoint,
operationName(if provided), andrequest_idfrom response extensions when available. Emit debug logs describing whetherRetry-Afterparsing succeeded and which parser/format was used. Never log Authorization headers, tokens, or cookies. - Tests: unit coverage includes 429 retry with timestamp header, unparseable
Retry-After, past reset time (immediate retry), and no retries on 500/502/503. Integration tests must skip gracefully and, if a natural 429 occurs, confirm a single retry path and logging without intentionally exhausting rate limits.
- API models (
python/atlassian/graph/gen/,python/atlassian/rest/gen/,go/atlassian/graph/gen/,go/atlassian/rest/gen/) are generated from live schemas and match the API response shape for specific operations/endpoints. - Canonical models (
python/atlassian/canonical_models.py,go/atlassian/canonical_models.go) are stable, versioned analytics schemas. Sources of truth:- Jira:
openapi/jira-developer-health.canonical.openapi.yaml - Compass:
openapi/compass-developer-health.canonical.openapi.yaml - Teams:
openapi/teams-developer-health.canonical.openapi.yaml
- Jira:
- Mappers live in
python/atlassian/graph/mappers/,python/atlassian/rest/mappers/,go/atlassian/graph/mappers/, andgo/atlassian/rest/mappers/.
- Python:
cd python && pip install -e . && pytest - Go:
cd go && go test ./... - Terraform:
cd terraform && go test ./...(ormake terraform-test) - Integration (env-gated):
ATLASSIAN_CLOUD_ID(orATLASSIAN_JIRA_CLOUD_ID) for Jira project listing integration tests- One of
ATLASSIAN_OAUTH_ACCESS_TOKENor (ATLASSIAN_EMAIL+ATLASSIAN_API_TOKEN) orATLASSIAN_COOKIES_JSON - Optional OAuth auto-refresh:
ATLASSIAN_OAUTH_REFRESH_TOKEN+ATLASSIAN_CLIENT_ID+ATLASSIAN_CLIENT_SECRET ATLASSIAN_GQL_BASE_URLis required for non-OAuth auth modes; OAuth defaults tohttps://api.atlassian.com- Jira REST base URL: set
ATLASSIAN_JIRA_BASE_URLfor tenanted auth; OAuth defaults tohttps://api.atlassian.com/ex/jira/{cloudId} - Jira REST issue search integration:
ATLASSIAN_JIRA_JQL - Jira REST history integration:
ATLASSIAN_JIRA_ISSUE_KEY(for changelog/worklog smoke tests) - Optional:
ATLASSIAN_GQL_EXPERIMENTAL_APIS(comma-separated; sent as repeatedX-ExperimentalApiheaders) - Integration tests will load a repo-root
.envif present (without overriding existing environment variables) - Python:
cd python && pytest tests/integration - Go:
cd go && go test -tags=integration ./...
A Terraform provider is available for reading Jira data using the Go client. See terraform/README.md for details.
Quick start:
provider "jira" {
cloud_id = "your-cloud-id"
}
data "jira_projects" "software" {
project_types = ["SOFTWARE"]
}
data "jira_issues" "recent" {
jql = "project = PROJ AND updated >= -7d"
}Build the provider:
make terraform