Synchronous and asynchronous Python client for the Photon API, generated from the
public OpenAPI contract. Requires Python 3.11+ and imports as photon_api.
pip install photonhq-apiimport os
from photon_api import Photon
from photon_api.rpc_generated import CountProjectsInput
token = os.environ["PHOTON_API_TOKEN"]
request = CountProjectsInput.model_validate(
{"path": {"organizationId": os.environ["PHOTON_ORGANIZATION_ID"]}}
)
with Photon(headers={"Authorization": f"Bearer {token}"}) as photon:
result = photon.organizations.projects.count(request)
print(result.count)countProjects accepts an Account Service Key or an Organization Service Identity
API key / M2M access token. Set PHOTON_API_TOKEN to one of those credentials
and PHOTON_ORGANIZATION_ID to the ID of an organization it can access. See the
Photon dashboard and documentation to create credentials and find IDs.
Every credential is sent as Authorization: Bearer <credential>. Pass it through
the headers option. The client does not choose or check a credential type; the
API accepts or rejects it.
| Credential | Security scheme | Notes |
|---|---|---|
Account Service Key (pho_ask_...) |
accountServiceKey |
Acts as the account and reaches every route the account can. |
Project API key (pho_sk_...) |
projectApiKey |
Bound to one project. Accepted only under /v1/projects/{projectId}. |
| Organization Service Identity API key or M2M access token | serviceIdentityBearer |
Restricted to its organization and explicitly granted permissions. Never send an M2M client secret to an API endpoint. |
| OAuth access token | oauth2 |
Scope is intersected with the permissions the route grants; an empty intersection returns 403 insufficient_scope. |
Each operation's security entry in the OpenAPI contract lists the credential
types it accepts. A few operations accept requests without credentials.
The SDK does not run OAuth flows. Obtain OAuth access tokens and manage refresh in your application's own authentication flow, then supply the current token.
headers accepts a mapping or a callable returning one. The callable runs before
every attempt, including retries, so it can return a freshly refreshed token.
For AsyncPhoton the callable may be async; for Photon it must be synchronous.
import asyncio
from photon_api import AsyncPhoton
async def main() -> None:
async with AsyncPhoton(headers={"Authorization": f"Bearer {token}"}) as photon:
result = await photon.organizations.projects.count(request)
print(result.count)
asyncio.run(main())Photonis synchronous;AsyncPhotonhas the same methods as coroutines.- Operations are grouped into namespaces that follow the API paths, for example
photon.organizations.projects.count. Namespaces and methods are snake_case, as inphoton.projects.agent_profile.get. Each method takes one input model fromphoton_api.rpc_generated(for exampleCountProjectsInput) withpath,query,bodyand header members as the operation declares. - The normal facade returns the response model.
photon.rawhas the same methods and returns aRawResponsewithdata,status,headersandrequest_id(thex-request-idresponse header when present). - Close the client to release connections: use
with/async with, or callclose()/await close(). A client you pass in withclient=is not closed for you.
Request inputs and responses are Pydantic models typed by the contract.
Building an input model checks types and required fields and raises
pydantic.ValidationError if they do not match; limits such as patterns,
lengths and ranges are checked by the API. Successful responses are parsed into
the model declared for their status. Fields the SDK does not know are kept, and
enums are open (Literal["a", "b"] | str), so the client keeps working when the
API adds fields or values. Dates and date-times are strings, as sent.
Omit optional fields that you do not want to send. Pass None only when the
schema permits null. Optional fields without defaults use Pydantic's MISSING
sentinel; check model_fields_set to see which fields were supplied. Request
serialization excludes omitted fields while preserving explicit nulls where
allowed. Extra fields are retained and sent.
The pinned Pydantic release marks MISSING experimental. Models containing it
cannot be pickled, and static type checker support is limited. JSON serialization
is supported; use model_dump(mode="json", by_alias=True, exclude_unset=True).
All errors extend PhotonError, which carries operation_id and request_id
when known.
| Class | Raised when | Attributes |
|---|---|---|
ApiError |
The API returns a non-success status. The message is the problem detail when present. |
status, headers, body (parsed JSON, or None), raw_body, operation_id, request_id |
ResponseValidationError |
A successful response has an undocumented status, is not empty when it should be, or does not match its model. | status, raw_body, operation_id, request_id |
TransportError |
The request failed at the HTTP layer (connection error, timeout) on the last attempt. | operation_id; the httpx error is in __cause__ |
from photon_api import ApiError
try:
photon.organizations.projects.count(request)
except ApiError as error:
print(error.status, error.request_id, error.body)
raise| Argument | Default | Behavior |
|---|---|---|
timeout |
30.0 |
Seconds, applied to each attempt. |
max_attempts |
3 |
Total attempts, including the first. Values are clamped to 1 to 3; 1 disables retries. |
max_retry_after |
60.0 |
Longest Retry-After, in seconds, the client waits for. |
- Only
GEToperations and requests that carry anIdempotency-Keyheader (from the input or from theheadersoption) are retried. Other mutations are sent once. - Retryable requests are retried after status 408, 429, 502, 503 or 504, or after
an
httpxerror (for example a connection error or timeout). The status list and backoff are not configurable. - Without
Retry-After, the delay is random between 0 andmin(2.0, 0.25 * 2^(attempt - 1))seconds. Retry-After(seconds or an HTTP date) is used as the delay. If it exceedsmax_retry_after, the client stops retrying and handles that response (anApiErrorfor an error status).- When attempts run out, the last response is handled normally, or
TransportErroris raised.
You can pass your own httpx.Client (for Photon) or httpx.AsyncClient (for
AsyncPhoton) as client=; the base URL, timeout and retry behavior above
still apply.
The client uses https://api.photon.codes. For tests and alternative environments,
set base_url:
photon = Photon(
base_url="http://localhost:8080",
headers={"Authorization": f"Bearer {token}"},
)The OpenAPI contract this client is generated from is
openapi/openapi.json
in photon-hq/api, with a Postman
collection generated from it. The same repository contains the
TypeScript,
Python and
Rust clients.
The TypeScript, Python and Rust clients share one version and are released together. Releases follow semantic versioning, starting at 0.1.0. Before 1.0, breaking changes increase the minor version. Changes are listed in CHANGELOG.md.
Generation requires Node.js 26 and no API credentials; Node.js is not needed to use the package. From the repository root:
python -m pip install -r tools/python-codegen/requirements-dev.txt
npm ci
npm run regenerate:python
python -m pip install -e packages/python
npm run test:pythonCI regenerates the client from the committed contract and fails if the output differs from the committed files.
Report bugs and requests through GitHub issues. Do not include credentials or private data. Report vulnerabilities privately as described in SECURITY.md.
This repository is generated, so external pull requests are not accepted. See CONTRIBUTING.md.