# jira2py > A lightweight, type-safe Python client for the Jira REST API # Getting Started # Installation ## Requirements - Python 3.11 or later - A Jira Cloud instance with API access - A Jira API token ([create one here](https://id.atlassian.com/manage-profile/security/api-tokens)) ## Installation ```bash pip install jira2py ``` ```bash uv add jira2py ``` ## Authentication jira2py uses [Jira API tokens](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/) with HTTP Basic authentication. The quickest way to get started is to set environment variables: ```bash export JIRA_URL="https://your-domain.atlassian.net" export JIRA_USER="your-email@example.com" export JIRA_API_TOKEN="your-api-token" ``` ```python from jira2py import JiraAPI jira = JiraAPI() # credentials loaded from environment ``` You can also pass credentials explicitly, or mix both approaches. See [Configuration](https://jira2py.org/guide/configuration/index.md) for the full details on credential resolution, validation, and precedence rules. ## Your First Script Here's a complete working example that fetches an issue, searches with JQL, and creates a new issue: ```python from jira2py import JiraAPI, JiraNotFoundError jira = JiraAPI() # Fetch a single issue issue = jira.issues.get_issue("PROJ-123", fields="summary,status,assignee") print(f"{issue['key']}: {issue['fields']['summary']}") print(f" Status: {issue['fields']['status']['name']}") # Search with JQL results = jira.search.enhanced_search( "project = PROJ AND status = 'In Progress' ORDER BY updated DESC", fields=["summary", "status", "assignee"], max_results=10, ) print(f"\nFound {results['total']} issues in progress:") for item in results["issues"]: print(f" {item['key']}: {item['fields']['summary']}") # Create a new issue new_issue = jira.issues.create_issue(fields={ "project": {"key": "PROJ"}, "issuetype": {"name": "Task"}, "summary": "Created with jira2py", "description": { "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ {"type": "text", "text": "This issue was created via the API."} ], } ], }, }) print(f"\nCreated issue: {new_issue['key']}") ``` Working with Atlassian Document Format (ADF) The `description` and comment `body` fields use Jira's [Atlassian Document Format](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/), which can be verbose to build by hand. Consider using a converter library to generate ADF from Markdown or other formats. For example: ```text - [marklassian](https://pypi.org/project/marklassian/) — Markdown to ADF - [pyadf](https://pypi.org/project/pyadf/) — ADF to Markdown Several other Python packages are available on PyPI for ADF conversion as well. ``` ```python # Handle errors gracefully try: jira.issues.get_issue("NONEXISTENT-999") except JiraNotFoundError: print("\nIssue not found (as expected)") ``` ## What's Next - [Configuration](https://jira2py.org/guide/configuration/index.md) — Retry settings, timeouts, and credential details - [Error Handling](https://jira2py.org/guide/error-handling/index.md) — Working with the exception hierarchy - [Rate Limiting](https://jira2py.org/guide/rate-limiting/index.md) — How automatic retry works - [API Reference](https://jira2py.org/api/index.md) — All available endpoints and their parameters # User Guide # Configuration ## Credentials `JiraAPI` requires three credentials to authenticate with your Jira Cloud instance: | Parameter | Environment Variable | Description | | ----------- | -------------------- | ------------------------------------------------------------------------------------------------------------- | | `url` | `JIRA_URL` | Base URL of your Jira instance | | `username` | `JIRA_USER` | Email address of your Atlassian account | | `api_token` | `JIRA_API_TOKEN` | API token from your [Atlassian account settings](https://id.atlassian.com/manage-profile/security/api-tokens) | ### Resolution Order Each credential is resolved independently using the same logic: 1. If an explicit argument is provided, it is used 1. Otherwise, the corresponding environment variable is checked 1. If neither is set, a `ValueError` is raised This means you can mix both methods — for example, keep the URL and username in the environment and pass only the token at runtime: ```python from jira2py import JiraAPI # JIRA_URL and JIRA_USER are set in the environment jira = JiraAPI(api_token="your-api-token") ``` ### Validation Credentials are validated at construction time: - All three values must be non-empty after stripping whitespace - The URL must start with `http://` or `https://` - A trailing `/` on the URL is removed automatically ```python from jira2py import JiraAPI # These all raise ValueError immediately JiraAPI(url="", username="user@example.com", api_token="token") # empty URL JiraAPI(url="not-a-url", username="user@example.com", api_token="token") # missing scheme JiraAPI() # no env vars set, no arguments provided ``` ### Immutability Credentials are immutable. Once a `JiraAPI` instance is created, its credentials cannot be changed. To use different credentials, create a new instance. ## Retry Settings jira2py automatically retries requests that receive an HTTP 429 (rate limit) response. You can configure this behavior through two parameters on `JiraAPI`: | Parameter | Default | Description | | ----------------- | ------- | ------------------------------------------------------------------------- | | `max_retries` | `4` | Maximum number of retry attempts on 429 responses. Set to `0` to disable. | | `max_retry_delay` | `30.0` | Upper bound on the wait time between retries, in seconds. | ```python jira = JiraAPI(max_retries=2, max_retry_delay=10.0) ``` See [Rate Limiting](https://jira2py.org/guide/rate-limiting/index.md) for the retry strategy, backoff behavior, and `Retry-After` header handling. ## HTTP Client The HTTP client is preconfigured with sensible defaults: | Setting | Value | | --------------- | ---------- | | HTTP/2 | Enabled | | Request timeout | 30 seconds | | Connect timeout | 10 seconds | Connections are reused across requests for better performance. Resources are cleaned up automatically when the Python process exits. # Error Handling All errors raised by jira2py are subclasses of `JiraError`, so you can catch everything with a single `except` clause or handle specific error types individually. ## Exception Hierarchy ```text JiraError ├── JiraAuthenticationError → 401, 403 ├── JiraConnectionError → timeouts, DNS failures, network errors └── JiraAPIError → HTTP 4xx / 5xx ├── JiraNotFoundError → 404 ├── JiraRateLimitError → 429 └── JiraValidationError → 400 ``` ## Catching Errors ### Catch everything ```python from jira2py import JiraAPI, JiraError jira = JiraAPI() try: issue = jira.issues.get_issue("PROJ-123") except JiraError as e: print(f"Something went wrong: {e.message}") ``` ### Handle specific error types ```python from jira2py import ( JiraAPI, JiraAuthenticationError, JiraConnectionError, JiraNotFoundError, JiraRateLimitError, JiraValidationError, ) jira = JiraAPI() try: jira.issues.edit_issue("PROJ-123", fields={"summary": "Updated"}) except JiraNotFoundError: print("Issue does not exist") except JiraValidationError as e: print(f"Invalid input: {e.error_messages}") except JiraAuthenticationError: print("Check your credentials or permissions") except JiraRateLimitError as e: print(f"Rate limited — retry after {e.retry_after}s") except JiraConnectionError: print("Network issue — check your connection") ``` ## Exception Attributes ### `JiraError` (base) All exceptions carry these attributes: | Attribute | Type | Description | | ---------- | ------------------ | -------------------------------------------- | | `message` | `str` | Human-readable error description | | `response` | `Response \| None` | The raw HTTP response object, when available | ### `JiraAPIError` and subclasses HTTP error exceptions add: | Attribute | Type | Description | | ---------------- | ----------- | ---------------------------------------------------- | | `status_code` | `int` | HTTP status code | | `error_messages` | `list[str]` | Error messages extracted from the Jira response body | ```python from jira2py import JiraAPIError try: jira.issues.create_issue(fields={"project": {"key": "INVALID"}}) except JiraAPIError as e: print(e.status_code) # 400 print(e.error_messages) # ["Field 'summary' is required", ...] ``` ### `JiraRateLimitError` Rate limit exceptions include additional diagnostic attributes: | Attribute | Type | Description | | ------------------- | --------------- | ------------------------------------------------------------------------- | | `retry_after` | `float \| None` | Seconds to wait, from the `Retry-After` header | | `rate_limit_reason` | `str \| None` | Which limit was hit (e.g., `jira-burst-based`, `jira-quota-tenant-based`) | | `reset_at` | `str \| None` | Timestamp when the rate limit window resets | See [Rate Limiting](https://jira2py.org/guide/rate-limiting/index.md) for how automatic retries work before this exception is raised. ## HTTP Status Mapping | Status Code | Exception | | ------------- | ------------------------- | | 400 | `JiraValidationError` | | 401 | `JiraAuthenticationError` | | 403 | `JiraAuthenticationError` | | 404 | `JiraNotFoundError` | | 429 | `JiraRateLimitError` | | Other 4xx | `JiraAPIError` | | 5xx | `JiraAPIError` | | Timeout | `JiraConnectionError` | | Network error | `JiraConnectionError` | ## Error Message Extraction jira2py automatically parses error details from Jira's response body. It looks for messages in these fields (in order): 1. `errorMessages` — a list of error strings 1. `errors` — a dictionary of field-level errors (values are extracted) 1. `message` — a single error string The extracted messages are available via the `error_messages` attribute on `JiraAPIError` and its subclasses. ## Exception Chaining All exceptions preserve the original cause via Python's exception chaining. You can access the underlying error through `__cause__`: ```python from jira2py import JiraConnectionError try: jira.issues.get_issue("PROJ-123") except JiraConnectionError as e: print(f"jira2py error: {e.message}") print(f"Original cause: {e.__cause__}") ``` For the full exception class reference, see [Exceptions](https://jira2py.org/api/exceptions/index.md). # Rate Limiting Jira Cloud enforces [rate limits](https://developer.atlassian.com/cloud/jira/platform/rate-limiting/) to protect the platform from excessive API usage. When a rate limit is hit, Jira responds with HTTP 429 (Too Many Requests). jira2py handles this automatically — no configuration required. Requests that receive a 429 response are retried transparently, and your code only sees an error if all retries are exhausted. ## How It Works When a request receives a 429 response: 1. The client checks the `Retry-After` header for a server-specified wait time 1. It calculates the delay using the appropriate backoff strategy (see below) 1. A warning is logged with the attempt number, `RateLimit-Reason`, and `Retry-After` values 1. The request is retried after the delay 1. If all retries are exhausted, a `JiraRateLimitError` is raised Only HTTP 429 responses are retried. All other errors — including 500, 401, 404, etc. — propagate immediately. ## Backoff Strategy The delay between retries depends on whether Jira includes a `Retry-After` header in the 429 response. ### With `Retry-After` header The server-specified wait time is used as the minimum delay. Additive jitter of 0–30% is applied above that minimum to avoid thundering herd problems when multiple clients are rate-limited simultaneously: ```text delay = retry_after + random(0, retry_after × 0.3) ``` ### Without `Retry-After` header Exponential backoff is used with a base delay of 5 seconds. Multiplicative jitter (0.7×–1.3×) is applied to spread out retries: ```text delay = 5 × 2^(attempt - 1) × random(0.7, 1.3) ``` This produces approximate delays of 5s, 10s, 20s, 40s for attempts 1 through 4. ### Delay cap Regardless of the strategy, the computed delay is capped at `max_retry_delay` (default: 30 seconds). ## Configuration Retry behavior is configured through `JiraAPI` constructor parameters. See [Configuration](https://jira2py.org/guide/configuration/index.md) for the parameter reference. ```python from jira2py import JiraAPI # Custom: fewer retries, shorter max wait jira = JiraAPI(max_retries=2, max_retry_delay=10.0) # Disable retries entirely jira = JiraAPI(max_retries=0) ``` ## Logging Retry attempts are logged at `WARNING` level via the `jira2py` logger. Each log message includes: - Attempt number - `RateLimit-Reason` header value (e.g., `jira-burst-based`, `jira-quota-tenant-based`) - `Retry-After` header value To see retry logs: ```python import logging logging.basicConfig(level=logging.WARNING) ``` ## When Retries Are Exhausted If all retry attempts fail, a `JiraRateLimitError` is raised. See [Error Handling](https://jira2py.org/guide/error-handling/index.md) for how to catch it and inspect its diagnostic attributes (`retry_after`, `rate_limit_reason`, `reset_at`). # API Reference # API Reference All Jira operations are accessed through the `JiraAPI` facade. Each API module is available as a property: ```python from jira2py import JiraAPI jira = JiraAPI() jira.issues # Issues — get, create, edit, metadata jira.search # Issue Search — JQL queries jira.comments # Issue Comments — list, add jira.fields # Issue Fields — list system and custom fields jira.issue_links # Issue Links — link types, create, delete jira.projects # Projects — search and list jira.attachments # Attachments — metadata jira.users # Users — search ``` ## Modules | Module | Property | Description | | ----------------------------------------------------------------- | ------------------ | -------------------------------------------------------------------- | | [JiraAPI](https://jira2py.org/api/jira-api/index.md) | — | Entry point and facade | | [Issues](https://jira2py.org/api/issues/index.md) | `jira.issues` | Create, read, and update issues; changelogs and create/edit metadata | | [Issue Search](https://jira2py.org/api/issue-search/index.md) | `jira.search` | Search issues with JQL | | [Issue Comments](https://jira2py.org/api/issue-comments/index.md) | `jira.comments` | List and add comments | | [Issue Fields](https://jira2py.org/api/issue-fields/index.md) | `jira.fields` | List system and custom fields | | [Issue Links](https://jira2py.org/api/issue-links/index.md) | `jira.issue_links` | List link types, create and delete links | | [Projects](https://jira2py.org/api/projects/index.md) | `jira.projects` | Search and list projects | | [Attachments](https://jira2py.org/api/attachments/index.md) | `jira.attachments` | Get attachment metadata | | [Users](https://jira2py.org/api/users/index.md) | `jira.users` | Search users by name or email | | [Exceptions](https://jira2py.org/api/exceptions/index.md) | — | Exception hierarchy | ## Conventions ### `extra_params` and `extra_data` Most methods accept two optional keyword arguments for extensibility: - **`extra_params`** — Additional query parameters merged into the request URL. Named parameters take precedence over `extra_params` if there's a key conflict. - **`extra_data`** — Additional fields merged into the request body. Named data fields take precedence over `extra_data` if there's a key conflict. These allow you to use Jira REST API parameters that jira2py doesn't expose as named arguments: ```python issue = jira.issues.get_issue( "PROJ-123", extra_params={"fieldsByKeys": True, "properties": "myProp"}, ) ``` ### Return types - Methods that return data give back `dict[str, Any]` or `list[dict[str, Any]]` — the parsed JSON from the Jira REST API. - Methods for operations with no response body (e.g., delete, create link) return `None`. - Paginated endpoints return the full response dict including `startAt`, `maxResults`, `total`, and the results list. # Attachments Accessed via `jira.attachments`. Retrieve metadata for issue attachments. ## `get_attachment_metadata` Get metadata for a specific attachment. Attachment IDs can be found in the `attachment` field of an issue. ```python # Find attachment IDs from an issue issue = jira.issues.get_issue("PROJ-123", fields="attachment") for att in issue["fields"]["attachment"]: print(f"{att['id']}: {att['filename']}") # Get detailed metadata for a specific attachment metadata = jira.attachments.get_attachment_metadata("10001") print(f"File: {metadata['filename']}") print(f"Size: {metadata['size']} bytes") print(f"Type: {metadata['mimeType']}") print(f"URL: {metadata['content']}") ``` | Parameter | Type | Default | Description | | --------------- | --------------------------- | -------- | ------------------------------- | | `attachment_id` | `str` | required | Attachment ID (e.g., `"10001"`) | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — attachment metadata including `id`, `filename`, `size`, `mimeType`, `content` (download URL), `author`, and `created`. [Jira REST API — Get attachment metadata](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-attachments/#api-rest-api-3-attachment-id-get) # Exceptions All exceptions are importable from the top-level package: ```python from jira2py import ( JiraError, JiraAuthenticationError, JiraConnectionError, JiraAPIError, JiraNotFoundError, JiraRateLimitError, JiraValidationError, ) ``` For usage patterns and examples, see [Error Handling](https://jira2py.org/guide/error-handling/index.md). ## Hierarchy ```text JiraError ├── JiraAuthenticationError ├── JiraConnectionError └── JiraAPIError ├── JiraNotFoundError ├── JiraRateLimitError └── JiraValidationError ``` ## `JiraError` Base exception for all jira2py errors. | Attribute | Type | Description | | ---------- | ------------------ | ------------------------------------- | | `message` | `str` | Human-readable error description | | `response` | `Response \| None` | The raw HTTP response, when available | ## `JiraAuthenticationError` Raised when authentication or authorization fails (HTTP 401, 403). Inherits all attributes from [`JiraError`](#jiraerror). ## `JiraConnectionError` Raised on network-level failures: timeouts, DNS resolution errors, connection refused. Inherits all attributes from [`JiraError`](#jiraerror). ## `JiraAPIError` Raised for HTTP 4xx and 5xx responses not covered by a more specific exception. | Attribute | Type | Description | | ---------------- | ----------- | ---------------------------------------------------- | | `status_code` | `int` | HTTP status code | | `error_messages` | `list[str]` | Error messages extracted from the Jira response body | Also inherits `message` and `response` from [`JiraError`](#jiraerror). ## `JiraNotFoundError` Raised when a requested resource is not found (HTTP 404). Inherits all attributes from [`JiraAPIError`](#jiraapierror). ## `JiraRateLimitError` Raised when the API rate limit is exceeded (HTTP 429) and all retries have been exhausted. | Attribute | Type | Description | | ------------------- | --------------- | ------------------------------------------------------------------------- | | `retry_after` | `float \| None` | Seconds the server asked to wait | | `rate_limit_reason` | `str \| None` | Which limit was hit (e.g., `jira-burst-based`, `jira-quota-tenant-based`) | | `reset_at` | `str \| None` | Timestamp when the rate limit window resets | Also inherits all attributes from [`JiraAPIError`](#jiraapierror). See [Rate Limiting](https://jira2py.org/guide/rate-limiting/index.md) for how automatic retries work before this exception is raised. ## `JiraValidationError` Raised when request validation fails (HTTP 400). Typically indicates malformed input or missing required fields. Inherits all attributes from [`JiraAPIError`](#jiraapierror). # Issue Comments Accessed via `jira.comments`. List and add comments on issues. ## `get_comments` Get comments for an issue. Returns paginated results. ```python comments = jira.comments.get_comments("PROJ-123") print(f"Total comments: {comments['total']}") for comment in comments["comments"]: print(f"{comment['author']['displayName']}: {comment['body']}") ``` ```python # Most recent comments first comments = jira.comments.get_comments("PROJ-123", order_by="-created") ``` | Parameter | Type | Default | Description | | -------------- | --------------------------- | -------- | ------------------------------------------------------------------- | | `issue_id` | `str` | required | Issue ID or key | | `start_at` | `int` | `0` | Index of the first comment to return (0-based) | | `max_results` | `int` | `100` | Maximum number of comments | | `order_by` | `str \| None` | `None` | Sort order: `"created"`, `"-created"`, `"updated"`, or `"-updated"` | | `expand` | `str \| None` | `None` | Comma-separated fields to expand (e.g., `"renderedBody"`) | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — paginated response with `startAt`, `maxResults`, `total`, and `comments`. [Jira REST API — Get comments](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get) ______________________________________________________________________ ## `add_comment` Add a comment to an issue. The comment body must be in [Atlassian Document Format (ADF)](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/). ```python comment = jira.comments.add_comment("PROJ-123", body={ "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ {"type": "text", "text": "This is a comment from jira2py."} ], } ], }) print(f"Comment added: {comment['id']}") ``` ```python # Restrict visibility to a role comment = jira.comments.add_comment( "PROJ-123", body={ "type": "doc", "version": 1, "content": [ { "type": "paragraph", "content": [ {"type": "text", "text": "Internal note."} ], } ], }, visibility={"type": "role", "value": "Administrators"}, ) ``` | Parameter | Type | Default | Description | | -------------- | --------------------------- | -------- | ---------------------------------------------------------------------------- | | `issue_id` | `str` | required | Issue ID or key | | `body` | `Mapping[str, Any]` | required | Comment body in ADF format | | `visibility` | `Mapping[str, Any] \| None` | `None` | Visibility restriction (e.g., `{"type": "role", "value": "Administrators"}`) | | `expand` | `str \| None` | `None` | Comma-separated fields to expand | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | | `extra_data` | `Mapping[str, Any] \| None` | `None` | Additional request body data | **Returns:** `dict[str, Any]` — created comment details including `id`, `body`, `author`, and `created`. Tip Building ADF by hand can be verbose. Libraries like [marklassian](https://pypi.org/project/marklassian/) can convert Markdown to ADF for you. [Jira REST API — Add comment](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post) # Issue Fields Accessed via `jira.fields`. List system and custom fields configured in your Jira instance. ## `get_fields` Get all system and custom issue fields. ```python fields = jira.fields.get_fields() for field in fields: kind = "custom" if field["custom"] else "system" print(f"{field['id']}: {field['name']} ({kind})") ``` ```python # Find a specific custom field by name target = "Story Points" match = next((f for f in jira.fields.get_fields() if f["name"] == target), None) if match: print(f"Use field ID '{match['id']}' for {target}") ``` This method takes no parameters. **Returns:** `list[dict[str, Any]]` — list of field objects with `id`, `name`, `custom`, `schema`, and other properties. [Jira REST API — Get fields](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-fields/#api-rest-api-3-field-get) # Issue Links Accessed via `jira.issue_links`. List available link types, create links between issues, and delete existing links. ## `get_link_types` Get all available issue link types. ```python result = jira.issue_links.get_link_types() for link_type in result["issueLinkTypes"]: print(f"{link_type['name']}: {link_type['inward']} / {link_type['outward']}") ``` This method takes no parameters. **Returns:** `dict[str, Any]` — dictionary with `issueLinkTypes` containing link type objects. [Jira REST API — Get issue link types](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-link-types/#api-rest-api-3-issuelinktype-get) ______________________________________________________________________ ## `create_link` Create a link between two issues. ```python # "PROJ-1 blocks PROJ-2" jira.issue_links.create_link("Blocks", "PROJ-1", "PROJ-2") # "PROJ-3 is cloned by PROJ-4" jira.issue_links.create_link("Cloners", "PROJ-3", "PROJ-4") ``` Use [`get_link_types`](#get_link_types) to discover available link type names. | Parameter | Type | Default | Description | | ------------------- | ----- | -------- | ------------------------------------------------------------- | | `link_type_name` | `str` | required | Link type name (e.g., `"Blocks"`, `"Cloners"`, `"Duplicate"`) | | `inward_issue_key` | `str` | required | Key of the inward issue | | `outward_issue_key` | `str` | required | Key of the outward issue | **Returns:** `None` [Jira REST API — Create issue link](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-links/#api-rest-api-3-issuelink-post) ______________________________________________________________________ ## `delete_link` Delete an issue link. The link ID can be found in the `issuelinks` field of an issue. ```python issue = jira.issues.get_issue("PROJ-123", fields="issuelinks") for link in issue["fields"]["issuelinks"]: print(f"Link {link['id']}: {link['type']['name']}") # Delete a specific link jira.issue_links.delete_link("10001") ``` | Parameter | Type | Default | Description | | --------- | ----- | -------- | ------------------------------ | | `link_id` | `str` | required | ID of the issue link to delete | **Returns:** `None` [Jira REST API — Delete issue link](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-links/#api-rest-api-3-issuelink-linkid-delete) # Issue Search Accessed via `jira.search`. Search for issues using JQL (Jira Query Language). ## `enhanced_search` Search for issues using a JQL query. Returns paginated results. ```python # Basic search results = jira.search.enhanced_search("project = PROJ ORDER BY created DESC") print(f"Total results: {results['total']}") for issue in results["issues"]: print(f"{issue['key']}: {issue['fields']['summary']}") ``` ```python # Request specific fields results = jira.search.enhanced_search( "project = PROJ AND status = 'In Progress'", fields=["summary", "status", "assignee"], max_results=25, ) ``` ```python # Paginate through all results results = jira.search.enhanced_search("project = PROJ", max_results=100) while results.get("nextPageToken"): results = jira.search.enhanced_search( "project = PROJ", max_results=100, next_page_token=results["nextPageToken"], ) ``` | Parameter | Type | Default | Description | | ----------------- | --------------------------- | -------- | ------------------------------------------------------------------------- | | `jql` | `str` | required | JQL query string | | `next_page_token` | `str \| None` | `None` | Token for fetching the next page of results | | `max_results` | `int` | `50` | Maximum items per page | | `fields` | `list[str] \| None` | `None` | Fields to return (e.g., `["summary", "status"]`). Use `["*all"]` for all. | | `expand` | `str \| None` | `None` | Comma-separated properties to expand | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | | `extra_data` | `Mapping[str, Any] \| None` | `None` | Additional request body data | **Returns:** `dict[str, Any]` — search results with `issues`, `total`, and `nextPageToken`. [Jira REST API — Search for issues using JQL enhanced search](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-post) # Issues Accessed via `jira.issues`. Provides operations for creating, reading, and updating Jira issues, as well as retrieving changelogs and create/edit metadata. ## `get_issue` Get details of a specific issue. ```python issue = jira.issues.get_issue("PROJ-123") print(issue["fields"]["summary"]) # Request specific fields only issue = jira.issues.get_issue("PROJ-123", fields="summary,status,assignee") # Expand additional properties issue = jira.issues.get_issue("PROJ-123", expand="renderedFields,changelog") ``` | Parameter | Type | Default | Description | | -------------- | --------------------------- | -------- | --------------------------------------------------------------- | | `issue_id` | `str` | required | Issue ID or key (e.g., `"PROJ-123"`) | | `fields` | `str \| None` | `None` | Comma-separated field names. Use `"*all"` for all fields. | | `expand` | `str \| None` | `None` | Comma-separated properties to expand (e.g., `"renderedFields"`) | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — issue details including `key`, `fields`, `self`, etc. [Jira REST API — Get issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get) ______________________________________________________________________ ## `create_issue` Create a new issue. Requires at minimum `project`, `issuetype`, and `summary` fields. ```python new_issue = jira.issues.create_issue(fields={ "project": {"key": "PROJ"}, "issuetype": {"name": "Task"}, "summary": "New task", }) print(f"Created {new_issue['key']}") ``` Use [`get_create_issue_types`](#get_create_issue_types) and [`get_create_fields`](#get_create_fields) to discover which fields are available for a given project and issue type. | Parameter | Type | Default | Description | | ---------------- | --------------------------- | -------- | -------------------------------------------- | | `fields` | `Mapping[str, Any]` | required | Issue fields | | `update_history` | `bool` | `False` | Whether to add the project to browse history | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | | `extra_data` | `Mapping[str, Any] \| None` | `None` | Additional request body data | **Returns:** `dict[str, Any]` — created issue's `id`, `key`, and `self` URL. [Jira REST API — Create issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post) ______________________________________________________________________ ## `edit_issue` Update an existing issue's fields. ```python # Simple field update jira.issues.edit_issue("PROJ-123", fields={"summary": "Updated summary"}) # Update and return the modified issue updated = jira.issues.edit_issue( "PROJ-123", fields={"priority": {"name": "High"}}, return_issue=True, expand="renderedFields", ) print(updated["fields"]["priority"]["name"]) ``` | Parameter | Type | Default | Description | | -------------- | --------------------------- | -------- | --------------------------------------------------------- | | `issue_id` | `str` | required | Issue ID or key | | `fields` | `Mapping[str, Any]` | required | Fields to update | | `notify_users` | `bool` | `True` | Whether to send email notifications | | `return_issue` | `bool` | `False` | Whether to return the updated issue | | `expand` | `str \| None` | `None` | Properties to expand (only used with `return_issue=True`) | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | | `extra_data` | `Mapping[str, Any] \| None` | `None` | Additional request body data | **Returns:** `dict[str, Any] | None` — the updated issue if `return_issue=True`, otherwise `None`. [Jira REST API — Edit issue](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-put) ______________________________________________________________________ ## `get_changelogs` Get the changelog history for an issue. Returns a paginated response. ```python changelogs = jira.issues.get_changelogs("PROJ-123") print(f"Total changes: {changelogs['total']}") for entry in changelogs["values"]: print(f"{entry['created']} by {entry['author']['displayName']}") for item in entry["items"]: print(f" {item['field']}: {item['fromString']} → {item['toString']}") ``` | Parameter | Type | Default | Description | | -------------- | --------------------------- | -------- | ------------------------------------------- | | `issue_id` | `str` | required | Issue ID or key | | `start_at` | `int` | `0` | Index of the first item to return (0-based) | | `max_results` | `int` | `50` | Maximum number of results | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — paginated response with `startAt`, `maxResults`, `total`, `isLast`, and `values`. [Jira REST API — Get changelogs](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-changelog-get) ______________________________________________________________________ ## `get_edit_metadata` Get the fields available for editing on an issue. ```python meta = jira.issues.get_edit_metadata("PROJ-123") for field_key, field_info in meta["fields"].items(): print(f"{field_key}: {field_info['name']} ({field_info['schema']['type']})") ``` | Parameter | Type | Default | Description | | -------------------------- | --------------------------- | -------- | ---------------------------------------- | | `issue_id` | `str` | required | Issue ID or key | | `override_screen_security` | `bool` | `False` | Include fields hidden by screen security | | `override_editable_flag` | `bool` | `False` | Include non-editable fields | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — edit metadata including available fields and their schemas. [Jira REST API — Get edit issue metadata](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-editmeta-get) ______________________________________________________________________ ## `get_create_issue_types` Get the issue types available for creating issues in a project. ```python types = jira.issues.get_create_issue_types("PROJ") for issue_type in types["values"]: print(f"{issue_type['id']}: {issue_type['name']}") ``` | Parameter | Type | Default | Description | | ------------------- | --------------------------- | -------- | ---------------------------------- | | `project_id_or_key` | `str` | required | Project ID or key (e.g., `"PROJ"`) | | `start_at` | `int` | `0` | Index of the first item to return | | `max_results` | `int` | `50` | Maximum number of items | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — issue types available for the project. [Jira REST API — Get create issue metadata](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-types/#api-rest-api-3-issue-createmeta-projectidorkey-issuetypes-get) ______________________________________________________________________ ## `get_create_fields` Get the fields available when creating an issue of a specific type. Use [`get_create_issue_types`](#get_create_issue_types) first to discover the issue type ID. ```python fields = jira.issues.get_create_fields("PROJ", "10001") for field in fields["values"]: required = "required" if field.get("required") else "optional" print(f"{field['fieldId']}: {field['name']} ({required})") ``` | Parameter | Type | Default | Description | | ------------------- | --------------------------- | -------- | --------------------------------- | | `project_id_or_key` | `str` | required | Project ID or key | | `issue_type_id` | `str` | required | Issue type ID (e.g., `"10001"`) | | `start_at` | `int` | `0` | Index of the first item to return | | `max_results` | `int` | `50` | Maximum number of items | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — fields available for creating the issue type. [Jira REST API — Get create field metadata](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-types/#api-rest-api-3-issue-createmeta-projectidorkey-issuetypes-issuetypeid-get) # JiraAPI The main entry point for all Jira operations. ## Constructor ```python from jira2py import JiraAPI jira = JiraAPI( url: str | None = None, username: str | None = None, api_token: str | None = None, max_retries: int = 4, max_retry_delay: float = 30.0, ) ``` | Parameter | Default | Description | | ----------------- | ------- | ----------------------------------------------------------- | | `url` | `None` | Jira instance URL. Falls back to `JIRA_URL` env var. | | `username` | `None` | Atlassian account email. Falls back to `JIRA_USER` env var. | | `api_token` | `None` | API token. Falls back to `JIRA_API_TOKEN` env var. | | `max_retries` | `4` | Max retry attempts on HTTP 429. | | `max_retry_delay` | `30.0` | Max delay between retries in seconds. | See [Configuration](https://jira2py.org/guide/configuration/index.md) for credential resolution and [Rate Limiting](https://jira2py.org/guide/rate-limiting/index.md) for retry behavior. ## Properties | Property | Type | Description | | ------------- | ------------------------------------------------------------------ | ------------------------ | | `issues` | [`Issues`](https://jira2py.org/api/issues/index.md) | Issue operations | | `search` | [`IssueSearch`](https://jira2py.org/api/issue-search/index.md) | JQL search | | `comments` | [`IssueComments`](https://jira2py.org/api/issue-comments/index.md) | Issue comments | | `fields` | [`IssueFields`](https://jira2py.org/api/issue-fields/index.md) | System and custom fields | | `issue_links` | [`IssueLinks`](https://jira2py.org/api/issue-links/index.md) | Issue link operations | | `projects` | [`Projects`](https://jira2py.org/api/projects/index.md) | Project search | | `attachments` | [`Attachments`](https://jira2py.org/api/attachments/index.md) | Attachment metadata | | `users` | [`Users`](https://jira2py.org/api/users/index.md) | User search | ## Usage ```python from jira2py import JiraAPI jira = JiraAPI() # Access any module through the facade issue = jira.issues.get_issue("PROJ-123") results = jira.search.enhanced_search("project = PROJ") fields = jira.fields.get_fields() projects = jira.projects.search_projects() ``` # Projects Accessed via `jira.projects`. Search and list Jira projects. ## `search_projects` Search for projects with optional filtering and pagination. ```python # List all projects projects = jira.projects.search_projects() for project in projects["values"]: print(f"{project['key']}: {project['name']}") ``` ```python # Search by name or key projects = jira.projects.search_projects(query="backend") ``` ```python # Filter by specific keys projects = jira.projects.search_projects(keys=["PROJ", "INFRA", "OPS"]) ``` ```python # Include additional details projects = jira.projects.search_projects( expand="description,lead,issueTypes", ) for project in projects["values"]: print(f"{project['key']}: {project.get('description', '')}") print(f" Lead: {project['lead']['displayName']}") ``` | Parameter | Type | Default | Description | | -------------- | --------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- | | `start_at` | `int` | `0` | Index of the first item to return (0-based) | | `max_results` | `int` | `50` | Maximum items per page (max 100) | | `project_ids` | `list[int] \| None` | `None` | Filter by project IDs (up to 50) | | `keys` | `list[str] \| None` | `None` | Filter by project keys (up to 50) | | `query` | `str \| None` | `None` | Filter by matching key or name (case insensitive) | | `expand` | `str \| None` | `None` | Comma-separated properties to expand: `description`, `projectKeys`, `lead`, `issueTypes`, `url`, `insight` | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `dict[str, Any]` — paginated results with `startAt`, `maxResults`, `total`, `isLast`, and `values`. [Jira REST API — Search projects](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-search-get) # Users Accessed via `jira.users`. Search for Jira users by name or email. ## `search_users` Search for users matching a query string. ```python users = jira.users.search_users("john") for user in users: print(f"{user['displayName']} ({user['emailAddress']})") print(f" Account ID: {user['accountId']}") print(f" Active: {user['active']}") ``` ```python # Use account IDs for assigning issues users = jira.users.search_users("jane@example.com") if users: jira.issues.edit_issue("PROJ-123", fields={ "assignee": {"accountId": users[0]["accountId"]}, }) ``` | Parameter | Type | Default | Description | | -------------- | --------------------------- | -------- | ------------------------------------------- | | `query` | `str` | required | Search string for display name or email | | `start_at` | `int` | `0` | Index of the first item to return (0-based) | | `max_results` | `int` | `50` | Maximum results to return (max 1000) | | `extra_params` | `Mapping[str, Any] \| None` | `None` | Additional query parameters | **Returns:** `list[dict[str, Any]]` — list of user objects with `accountId`, `displayName`, `emailAddress`, and `active`. [Jira REST API — Find users](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-user-search/#api-rest-api-3-user-search-get)