JIRA AS
A Python library and CLI for JIRA REST API automation, providing HTTP client, configuration management, error handling, and utilities for the JIRA Assistant Skills Claude Code plugin.
Installation
pip install jira-as
With optional keyring support for secure credential storage:
pip install jira-as[keyring]
Features
- CLI (
jira-as): Command-line interface for JIRA operations - JiraClient: HTTP client with automatic retry logic and exponential backoff
- ConfigManager: Multi-source configuration (env vars > keychain > settings.local.json > settings.json > defaults)
- Error Handling: Exception hierarchy mapping HTTP status codes to domain exceptions
- Validators: Input validation for issue keys, project keys, JQL queries, URLs, and more
- Formatters: Output formatting for tables, JSON, CSV export
- ADF Helper: Atlassian Document Format conversion (markdown/text to ADF and back)
- Time Utils: JIRA time format parsing and formatting (e.g., '2h', '1d 4h 30m')
- Cache: SQLite-based caching with TTL support for API responses
- Credential Manager: Secure credential storage via system keychain or JSON fallback
- Mock Client: Full mock implementation for testing without JIRA access
Quick Start
Configuration
Set environment variables:
export JIRA_API_TOKEN="your-api-token" # Get from https://id.atlassian.com/manage-profile/security/api-tokens
export JIRA_EMAIL="your-email@company.com"
export JIRA_SITE_URL="https://your-company.atlassian.net"
CLI Usage
# Get an issue
jira-as issue get PROJ-123
# Search issues
jira-as search query "project = PROJ AND status = Open"
# Create an issue
jira-as issue create PROJ --summary "New task" --type Task
# Transition an issue
jira-as lifecycle transition PROJ-123 "In Progress"
# See all commands
jira-as --help
Library Usage
from jira_as import get_jira_client, handle_errors
@handle_errors
def main():
# Get a configured JIRA client (use as context manager)
with get_jira_client() as client:
# Fetch an issue
issue = client.get_issue('PROJ-123')
print(f"Summary: {issue['fields']['summary']}")
# Search issues with JQL
results = client.search_issues('project = PROJ AND status = Open')
for issue in results['issues']:
print(f"{issue['key']}: {issue['fields']['summary']}")
if __name__ == '__main__':
main()
Core Components
JiraClient
from jira_as import JiraClient
# Direct instantiation (prefer get_jira_client() for config management)
client = JiraClient(
base_url="https://your-company.atlassian.net",
email="your-email@company.com",
api_token="your-api-token"
)
# Use as context manager
with client:
issue = client.get_issue('PROJ-123')
client.create_issue(project_key='PROJ', summary='New issue', issue_type='Task')
client.transition_issue('PROJ-123', 'Done')
Error Handling
from jira_as import (
JiraError,
AuthenticationError,
PermissionError,
NotFoundError,
handle_errors
)
@handle_errors
def main():
# Exceptions are caught and formatted nicely
pass
# Or handle manually
try:
with get_jira_client() as client:
client.get_issue('INVALID-999')
except NotFoundError as e:
print(f"Issue not found: {e}")
except AuthenticationError as e:
print(f"Auth failed: {e}")
except JiraError as e:
print(f"JIRA error: {e}")
Validators
from jira_as import (
validate_issue_key,
validate_project_key,
validate_jql,
validate_url,
ValidationError
)
try:
key = validate_issue_key('PROJ-123') # Returns 'PROJ-123'
key = validate_issue_key('invalid') # Raises ValidationError
except ValidationError as e:
print(f"Invalid input: {e}")
ADF Helper
from jira_as import (
markdown_to_adf,
text_to_adf,
adf_to_text
)
# Convert markdown to ADF for JIRA
adf = markdown_to_adf("**Bold** and *italic* text")
# Convert plain text to ADF
adf = text_to_adf("Simple text content")
# Extract text from ADF
text = adf_to_text(adf_document)
Time Utils
from jira_as import (
parse_time_string,
format_seconds,
parse_relative_date
)
# Parse JIRA time format to seconds
seconds = parse_time_string('2h 30m') # 9000
# Format seconds to JIRA time format
time_str = format_seconds(9000) # '2h 30m'
# Parse relative dates
dt = parse_relative_date('yesterday')
dt = parse_relative_date('2025-01-15')
Mock Mode
For testing without JIRA access:
export JIRA_MOCK_MODE=true
jira-as issue get DEMO-85 # Returns mock data
import os
os.environ['JIRA_MOCK_MODE'] = 'true'
from jira_as import get_jira_client
with get_jira_client() as client: # Returns MockJiraClient
issue = client.get_issue('DEMO-85') # Mock data
Development
# Clone the repository
git clone https://github.com/grandcamel/jira-as.git
cd jira-as
# Install development dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Format code (black formats; ruff sorts imports)
black src tests
ruff check --fix src tests
# Type checking
mypy src
License
MIT License - see LICENSE for details.
Generic Surface
The api group exposes the pinned Jira Cloud platform v3, Jira Software and
Jira Service Management documents through as-engine. All three indexes are
primary. Start with jira-as help, jira-as api search sprint, or
jira-as api describe getIssue. Use help paging, help search, and
help agile for tagged operations; --tier platform|software|servicedesk
selects one document's help. --full expands descriptions, --examples shows
enrichment examples, and long help lists continue with --offset.
jira-as api --transport responder call getIssue --issueIdOrKey SBX-1
jira-as api call getIssue --issue-id-or-key PROJ-1
jira-as api call searchAndReconsileIssuesUsingJql --jql 'project = PROJ' --all --limit 100
jira-as api call searchAndReconsileIssuesUsingJqlPost --body @search.json --all
Parameters accept their exact published names and kebab aliases. Bodies come
from --body @file, --body - (stdin), or repeated --field path=value.
One page is returned by default. --all merges the tagged collection; its
--limit caps the total, while --maxResults or the body maxResults field
sets the request page size. For APIs with a limit parameter use
--parameter-limit alongside --all. JSON is the default call output;
--format table|markdown renders it. Errors are JSON on stderr with status,
messages, operation and note; exits distinguish usage (2), authentication (3),
permission (4), not found (5), server/transport (6), and conflict (7).
Automatic paging is unsupported for findBulkAssignableUsers, findAssignableUsers, findUsersWithAllPermissions, and findUsersWithBrowsePermission: they filter after slicing, so an empty page does not prove exhaustion. Use getAllUsers plus caller-side filtering. For getAllUsers/getAllUsersDefault, maxResults above 1000 refuses before sending. The supported bare-array endpoints advance by the sent page size and probe until an empty page; a short nonempty page continues.
Discovery and responder mode need no credentials. HTTP calls use the existing
JIRA_SITE_URL, JIRA_EMAIL, JIRA_API_TOKEN and configuration chain only
when a call is sent. JIRA_AS_TRANSPORT selects http, responder, cassette
(with JIRA_AS_CASSETTE), or simulation (optional JIRA_AS_SIMULATION_SEED).
JIRA_AS_RECORD records HTTP responses through the shared scrubber. Simulation
is stateful for the supported wrapper workflows. Risk-tagged operations preview
without sending until --confirm is supplied.
Published operation IDs that collide are corrected in each document's
identity.overlay.json; descriptions/notes preserve their original names and
routes for search. Platform IDs are unchanged. Other examples include
getSoftwareIssue, getBoardConfiguration, getServiceDeskArticles, and
getRequestAttachmentContent. These explicit corrections preserve every route.
Project scope
The Generic Surface checks generated scope tags against JIRA_ALLOWED_PROJECTS
before sending. An absent allowlist is unrestricted; an empty value denies scoped
calls. Body-only identity requires matching --project KEY, including bodies
read from files. Keyed updates also check any project change hidden in the body.
JQL requires a complete project restriction and supports literal AND predicates.
Site-level calls (including numeric board, sprint and service-desk routes) require
JIRA_ALLOW_SITE_OPERATIONS=true; the default is false. Discovery and help stay
settings-free. See project scope details.
Wrapper migration
JAS-49 retains 35 wrappers that need a workflow, local transform, cache, or
autocomplete affordance; 14 compatibility verbs retain their 1.x names. The
remaining 159 wrappers are migration hints: invoking one performs no transport
and exits 2; --help exits 0. Of these, 143 name an indexed replacement and
16 report retirement with no indexed replacement (JAS-64, decision 34). Use help migration or the wrapper table for the complete map.
The survivor groups are bulk, lifecycle, fields, ops, relationships, search,
time, dev, agile, and JSM. fields list, fields get, and fields cache warm
use the v2 cached instance metadata. api call --adf-field customfield_ID
explicitly converts Markdown to ADF; a warm textarea-field cache enables the
same conversion automatically. Stateful simulation exercises supported survivor
workflows without HTTP.
At 2.0.0, JAS-64 decision 34 retires admin automation and automation-template,
dev get-commits, and JSM asset commands; no commands remain deferred. Jira
attachment multipart/binary transport and generic risk enrichment remain pending
JAS-65; their migration hints do not claim those capabilities are available.
Destructive operations
Every indexed DELETE and the documented bulk delete, move, archive and removal
operations carry a risk level. api call defaults destructive and irreversible
operations to a JSON preview with zero requests (exit 0); add --confirm to send
the request through the normal scope guard. Inspect the level with api describe OPERATION, and browse irreversible operations with help risk.
For the 2.0.0 upgrade, see the release migration notes and the complete 1.2.0 client method mapping.
Instance fields cache
fields cache warm fetches instance metadata into
~/.cache/jira-as/v2/instance-fields.json, with a 24-hour TTL. Set
JIRA_FIELDS_CACHE_DIR (or jira.fields_cache_dir in configuration) to select
the directory; use separate directories for different Jira instances. Reads
never fetch metadata or migrate a 1.x cache. Missing, expired, or malformed
metadata is cold: fields list reports it, and api describe createIssue
explains that automatic textarea conversion is inactive. Explicit
api call createIssue --adf-field customfield_ID … still works with a cold
cache. For an offline warm-up, use fields cache warm --transport responder.
Rich text and notes
Platform v3 description/environment, comment body and worklog comment paths accept Markdown. Use real newlines in UTF-8 files:
jira-as api --transport responder call createIssue \
--field fields.project.key=SBX --field fields.summary=x \
--field fields.issuetype.name=Task --field fields.description=@notes.md
jira-as api --transport responder call addComment \
--issueIdOrKey SBX-1 --field body=@notes.md
Tagged reads render Markdown with lossless placeholders for unsupported ADF
nodes; --raw preserves the stored ADF. Use a JSON --body @request.json or
stdin for already encoded ADF or an explicit null. Bulk create converts the
static paths in each issueUpdates item supplied as JSON. Use --adf-field customfield_ID to convert a selected custom field; a warm instance-field cache
also converts textarea custom fields automatically. A cold cache leaves
unselected custom fields literal, and pre-encoded ADF passes through unchanged.
JSM request fields also pass through unchanged; its explicit
isAdfRequest=true mode requires caller-supplied ADF. JSM request comments
remain strings.
help adf, help fields, help project-types, help rate-limits and the other
topics render source-backed entries. api describe and errors carry relevant
notes. Removed issue-search operations name their /search/jql replacements;
use api describe searchForIssuesUsingJql or help search. The operationId
search continues to mean status discovery. Search hides deprecated operations
unless --include-deprecated is supplied; calling one warns on stderr.
Build
The product vendors pristine Base Documents and manifest pins in
src/jira_as/specs. The wheel hook keeps the source stamp and compiles all
three documents through as-engine into _generated/catalog.json and three
indexes. Editable builds persist the same indexes; sdists contain source
inputs and the hook, excluding compiled indexes. Nothing is fetched during
compilation or runtime. The dependency range is as-engine>=0.1.0a0,<0.2.
To rebuild local indexes after changing an overlay:
python -c "from as_engine.build import compile_product; compile_product('src/jira_as/specs', 'src/jira_as/_generated')"
python scripts/generate_paging_tags.py
Run the generator before compilation when paging changes. Generated paging
precedes hand paging overrides in the manifest. Refresh deliberately with
python scripts/refresh_base_documents.py --from-file platform=/path/to/document.json
(and similarly software or servicedesk). The script records an oasdiff
changelog beside each refreshed source and updates manifest pins last. Set
OASDIFF to choose the executable. Without --from-file it explicitly fetches
the manifest URLs; offline workflows must supply local files.
Compatibility Contract
The fourteen jira-host operations keep their recorded 1.x invocation and output
shapes on the generic engine path. The machine-readable contract, capture
provenance, scope rules and responder suite are described in
Compatibility Contract. Duration input accepts
both 2h30m and 2h 30m; issue update --format markdown|text|adf supplements
existing description auto-detection.
Cassettes and the live suite
Offline cassette tests replay every Compatibility Contract variant and the
Generic Surface with networking disabled. The SBX live suite is gated by
--live, supports an offline simulation rehearsal, and creates and cleans up
its own disposable issues. See recording, replay and drift checks
for the supervisor's host commands and fixture review process.
Split Mode
Run this same jira-as build in two places: the host serves calls and holds its
normal ConfigManager credentials; the container runs a credential-free client.
Use the same package version/build on both sides (jira-as --version and
jira-as serve --version report the same identity). There is no separate sidecar
package, install, or pin.
On the host, with the usual Jira credentials configured:
mkdir -m 700 -p /tmp/jira-sidecar
JIRA_ALLOWED_PROJECTS=SBX jira-as serve \
--socket /tmp/jira-sidecar/jira.sock \
--binding /path/to/seat-home \
--call-log /tmp/jira-sidecar/calls.jsonl
--binding reads jira-binding.json in the existing ADR 0014 form:
{"schema_version":1,"primary":"SBX","permitted":["SBX"]}. A supplied binding
must be valid even when an environment override is set. The effective allowlist
is JIRA_ALLOWED_PROJECTS (including an empty value), otherwise the binding's
permitted, otherwise configured allowed_projects, otherwise empty. Startup
prints the source and count on stderr. --allow-site/--no-allow-site overrides
JIRA_ALLOW_SITE_OPERATIONS or the configured site policy.
With that same build already present in your client image, mount the socket directory and run the client as the socket owner's UID (the socket is 0600):
docker run --rm --user "$(id -u):$(id -g)" \
--mount type=bind,src=/tmp/jira-sidecar,dst=/run/jira-sidecar,readonly \
-e JIRA_AS_TRANSPORT=socket -e JIRA_AS_SOCKET=/run/jira-sidecar/jira.sock \
-e JIRA_ALLOWED_PROJECTS=SBX YOUR_IMAGE_WITH_THIS_JIRA_AS_BUILD \
jira-as api call getIssue --issue-id-or-key SBX-1
Do not include Jira credentials in that image or pass them into the client. The client validates locally; the server validates again against its own indexes, derives project identity from the call, and enforces its own allowlist through the same guard. Each received call is recorded once with a timestamp, operation, method/path template, parameter names, safe project/issue identity and outcome. The call log omits bodies and arbitrary parameter values and is created 0600.
--socket defaults to $XDG_RUNTIME_DIR/jira-as.sock, or
/tmp/jira-as-<uid>.sock. Serve stays in the foreground, stops on SIGINT/SIGTERM,
and removes its own socket. Existing paths are refused. Keep the socket's parent
directory private; an old socket must be checked and removed by its owner before
restart.
For clients sharing the host network namespace, optional loopback TCP uses
jira-as serve --tcp 127.0.0.1:8765 --token-file /private/session-token --call-log /private/calls.jsonl. The owned regular token file must be 0600 and contain one
printable ASCII session token (an optional final newline is accepted). Configure
the client with JIRA_AS_SERVE_TCP=127.0.0.1:8765 and JIRA_AS_SERVE_TOKEN instead
of JIRA_AS_SOCKET. Ordinary container loopback does not reach host loopback;
the Unix socket mount is the default container path.
Split Mode currently refuses --output, binary downloads and multipart uploads;
local host file references are never an upload protocol. Frames are limited to
8 MiB, connection/frame timeouts default to 30 seconds, and calls are not retried
by the socket client. Engine tests can use as_engine.serve.fake_sidecar with a
Responder Surface through this exact socket seam, without credentials or HTTP.
Release files for jira-as 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jira_as-2.0.0.tar.gz | 793.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jira_as-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.0 MB
Release files / jira_as-2.0.0.tar.gz
| Download URL | jira_as-2.0.0.tar.gz |
|---|---|
| Size | 793.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ccb11e1861db664de7918fc583c6d0fd79e388fd36682329abd2b24a9e643e65
|
|
BLAKE2b-256 checksum How to use checksums |
da5fb6333ea2450738268832e26a02fc1493a99f8a67f1b2d01a72e229c8d4cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency logRelease files / jira_as-2.0.0-py3-none-any.whl
| Download URL | jira_as-2.0.0-py3-none-any.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5acb42f920ad1a5c03e1025284f02e9f2f4eafd44f0aa802a342f7159f834633
|
|
BLAKE2b-256 checksum How to use checksums |
998af9878f4356e2b305a02ff0e526790c38ab01c3fdfca0285169c6c0b33ff0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.
Transparency log