Minimal Python client for Jobber GraphQL API with OAuth 2.0 support
Project description
Jobber Python Client
Minimal Python client for Jobber GraphQL API with OAuth 2.0 support.
Features
- One-time authentication: Browser-based OAuth flow stores tokens in Doppler
- Visual confirmation URLs: Get web links to verify API operations in Jobber UI (Quick Win Guide)
- Fail-fast errors: All failures raise exceptions with context
- Token auto-refresh: Transparent token refresh before expiration
- Rate limit awareness: Exposes throttle status, raises before exceeding limits
- Minimal dependencies: Only
requestsandoauthlibrequired
Installation
# Install from PyPI
pip install jobber-python-client
# Or clone repository for development
git clone https://github.com/tainora/jobber-python-client.git
cd jobber-python-client
uv sync
Prerequisites
- Jobber Developer Account: Create app at https://developer.getjobber.com/
- Doppler CLI: Install from https://docs.doppler.com/docs/install-cli
- OAuth Credentials: Store in Doppler:
doppler secrets set JOBBER_CLIENT_ID="your_client_id" \ JOBBER_CLIENT_SECRET="your_client_secret" \ --project claude-config --config dev
Quick Start
Step 1: Authenticate (one-time)
uv run jobber_auth.py
This will:
- Open browser for Jobber authorization
- Exchange authorization code for tokens
- Store tokens in Doppler
Step 2: Use in AI agent code
from jobber import JobberClient
# Create client (loads credentials from Doppler)
client = JobberClient.from_doppler()
# Execute GraphQL queries
result = client.execute_query("""
query {
clients(first: 10) {
nodes {
id
firstName
lastName
}
totalCount
}
}
""")
print(f"Total clients: {result['clients']['totalCount']}")
Visual Confirmation URLs (Quick Win!)
Problem: API operations feel abstract. You create a client via API, but can you see it in Jobber's web interface?
Solution: YES! Include jobberWebUri in your queries to get clickable web links.
Example: Create Client with Web Link
from jobber import JobberClient
client = JobberClient.from_doppler()
# IMPORTANT: Include jobberWebUri in mutation response
mutation = """
mutation CreateClient($input: ClientCreate!) {
clientCreate(input: $input) {
client {
id
firstName
lastName
jobberWebUri # ← Returns web URL!
}
}
}
"""
result = client.execute_query(mutation, {
'input': {
'firstName': 'John',
'lastName': 'Doe'
}
})
created = result['clientCreate']['client']
# Show clickable link for visual confirmation
print(f"✅ Client created: {created['firstName']} {created['lastName']}")
print(f"🔗 View in Jobber: {created['jobberWebUri']}")
# Click link → See client in Jobber web UI → Instant verification!
Available URL Fields
| Field | Available On | Purpose |
|---|---|---|
jobberWebUri |
Most resources (Client, Job, Quote, Invoice) | Direct link to resource in Jobber web UI |
previewUrl |
Quotes | Client Hub link for customer approval |
Quick Start
Run the complete example:
uv run examples/visual_confirmation_urls.py
Learn more: Visual Confirmation URLs Guide - Comprehensive patterns, best practices, and use cases.
Pro tip: ALWAYS include jobberWebUri in mutations for instant visual verification!
API Reference
JobberClient
Main client for executing GraphQL queries.
JobberClient.from_doppler(project, config)
Create client loading credentials from Doppler.
Parameters:
project(str): Doppler project name (default: "claude-config")config(str): Doppler config name (default: "dev")
Returns: JobberClient instance
Raises:
ConfigurationError: Doppler secrets not foundAuthenticationError: Token loading fails
Example:
client = JobberClient.from_doppler("claude-config", "dev")
client.execute_query(query, variables=None, operation_name=None)
Execute GraphQL query.
Parameters:
query(str): GraphQL query stringvariables(dict, optional): Query variablesoperation_name(str, optional): Operation name for multi-operation queries
Returns: dict - Response data (response['data'])
Raises:
AuthenticationError: Token invalid or expiredRateLimitError: Rate limit threshold exceeded (< 20% points available)GraphQLError: Query execution failedNetworkError: HTTP request failed
Example:
result = client.execute_query(
query="""
query GetClients($first: Int!) {
clients(first: $first) {
nodes { id firstName }
}
}
""",
variables={'first': 50}
)
client.get_throttle_status()
Get last known rate limit status.
Returns: dict or None
currentlyAvailable: Points available nowmaximumAvailable: Total capacity (typically 10,000)restoreRate: Points restored per second (typically 500)
Example:
status = client.get_throttle_status()
if status:
print(f"{status['currentlyAvailable']} points available")
Error Handling
All methods raise exceptions on failure. Handle appropriately:
from jobber import (
JobberClient,
AuthenticationError,
RateLimitError,
GraphQLError,
NetworkError,
)
try:
client = JobberClient.from_doppler()
result = client.execute_query("{ clients { totalCount } }")
except AuthenticationError as e:
# Resolution: Run jobber_auth.py
print(f"Auth error: {e}")
except RateLimitError as e:
# Resolution: Wait for points to restore
wait_seconds = e.context.get('wait_seconds', 0)
print(f"Rate limited. Wait {wait_seconds:.1f}s")
except GraphQLError as e:
# Resolution: Check query syntax
print(f"Query error: {e.errors}")
except NetworkError as e:
# Resolution: Check connectivity
print(f"Network error: {e}")
Examples
See examples/ directory:
basic_usage.py: Simple queries and paginationerror_handling.py: Comprehensive error handling patterns
Run examples:
uv run --with . examples/basic_usage.py
Architecture
See ADR-0001: Jobber API Client Architecture for design decisions.
Key Principles
- Fail-fast: Raise exceptions immediately, no retry or fallback
- Caller control: AI agent decides error recovery strategy
- Minimal abstraction: Thin wrapper over GraphQL HTTP requests
- Explicit configuration: No default values or silent assumptions
Module Structure
jobber/
├── __init__.py # Public API exports
├── client.py # JobberClient class
├── auth.py # TokenManager (Doppler integration)
├── graphql.py # GraphQLExecutor (HTTP requests)
└── exceptions.py # Exception hierarchy
Rate Limiting
Jobber API limits:
- 10,000 points available
- 500 points/second restore rate
- Query costs vary (9-50 points typical)
This library:
- Raises
RateLimitErrorwhen available points < 20% of maximum - Exposes
throttle_statusin exception context - Caller decides when to wait or abort
Token Management
Tokens expire after 60 minutes. This library:
- Proactively refreshes 5 minutes before expiration (background thread)
- Reactively refreshes on 401 errors (retry once)
- Updates Doppler with new tokens automatically
Token storage in Doppler:
JOBBER_ACCESS_TOKEN=eyJhbGc...
JOBBER_REFRESH_TOKEN=def502...
JOBBER_TOKEN_EXPIRES_AT=1731873600
SLOs
- Availability: Simple code paths, minimal failure modes
- Correctness: Type-safe responses, validation at boundaries
- Observability: Exceptions include full context and stack traces
- Maintainability: < 500 LOC core library, clear contracts
Development
Running Tests
pytest -v
Type Checking
mypy jobber/
Linting
ruff check jobber/
Contributing
This project uses Conventional Commits and semantic-release.
Commit format:
feat: add custom field support
fix: handle token refresh race condition
docs: update error handling examples
License
MIT
Support
- Jobber API Docs: https://developer.getjobber.com/docs/
- API Support: api-support@getjobber.com
- Issues: https://github.com/tainora/jobber-python-client/issues
Changelog
See CHANGELOG.md (auto-generated by semantic-release).
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file jobber_python_client-0.2.1.tar.gz.
File metadata
- Download URL: jobber_python_client-0.2.1.tar.gz
- Upload date:
- Size: 268.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9af5a344779040480013bc95b76ec52f8848e657673c9559f6c84452b5df162
|
|
| MD5 |
40bddd17cc62bcb1d346e30f1e1c5dc4
|
|
| BLAKE2b-256 |
f2d9f49c6ba647ef279191cfbafd9f097c365284195e5bc9ca8aaeb4d51413cb
|
File details
Details for the file jobber_python_client-0.2.1-py3-none-any.whl.
File metadata
- Download URL: jobber_python_client-0.2.1-py3-none-any.whl
- Upload date:
- Size: 21.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.11 {"installer":{"name":"uv","version":"0.9.11"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
442ca5fb4d17cbb6945f7fdfcca81787ec53f79957dd1829d2157990b522c41c
|
|
| MD5 |
bb1aa96674aa93d1fa3dc3ce6e3372a7
|
|
| BLAKE2b-256 |
a254e19ad748e3136b1069e5f91b96aa10e6d79b0f5927b83dd462ad767140b0
|