photonhq-api 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- photonhq_api-0.1.0/.gitignore +16 -0
- photonhq_api-0.1.0/LICENSE +22 -0
- photonhq_api-0.1.0/PKG-INFO +234 -0
- photonhq_api-0.1.0/README.md +220 -0
- photonhq_api-0.1.0/pyproject.toml +42 -0
- photonhq_api-0.1.0/src/photon_api/__init__.py +18 -0
- photonhq_api-0.1.0/src/photon_api/_model_base.py +62 -0
- photonhq_api-0.1.0/src/photon_api/client.py +79 -0
- photonhq_api-0.1.0/src/photon_api/config_generated.py +2 -0
- photonhq_api-0.1.0/src/photon_api/errors.py +62 -0
- photonhq_api-0.1.0/src/photon_api/generated/__init__.py +1 -0
- photonhq_api-0.1.0/src/photon_api/generated/models.py +29622 -0
- photonhq_api-0.1.0/src/photon_api/py.typed +0 -0
- photonhq_api-0.1.0/src/photon_api/rpc_generated.py +7128 -0
- photonhq_api-0.1.0/src/photon_api/transport.py +344 -0
- photonhq_api-0.1.0/tests/test_client.py +913 -0
- photonhq_api-0.1.0/tests/test_facade_validation.py +49 -0
- photonhq_api-0.1.0/tests/test_models.py +180 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Photon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: photonhq-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generated Photon API RPC client
|
|
5
|
+
Project-URL: Repository, https://github.com/photon-hq/api
|
|
6
|
+
Author: Photon
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: email-validator<3,>=2.3.0
|
|
11
|
+
Requires-Dist: httpx<1,>=0.28.1
|
|
12
|
+
Requires-Dist: pydantic<2.14,>=2.13.5
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# photonhq-api
|
|
16
|
+
|
|
17
|
+
Synchronous and asynchronous Python client for the Photon API, generated from the
|
|
18
|
+
public OpenAPI contract. Requires Python 3.11+ and imports as `photon_api`.
|
|
19
|
+
|
|
20
|
+
> **Preview release.** This version is generated from the current production
|
|
21
|
+
> contract before all of its schemas have stable names. Types the contract
|
|
22
|
+
> does not name yet have names derived from their operation (for example
|
|
23
|
+
> `ListProjectsResponse200ApplicationJson`) or carry a version prefix such as
|
|
24
|
+
> `Photon20260701_`. These type names change in 0.2.0, when the contract
|
|
25
|
+
> names them; the rename does not change requests, responses or method names.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install photonhq-api
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import os
|
|
37
|
+
|
|
38
|
+
from photon_api import Photon
|
|
39
|
+
from photon_api.rpc_generated import CountProjectsInput
|
|
40
|
+
|
|
41
|
+
token = os.environ["PHOTON_API_TOKEN"]
|
|
42
|
+
request = CountProjectsInput.model_validate(
|
|
43
|
+
{"path": {"organizationId": os.environ["PHOTON_ORGANIZATION_ID"]}}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with Photon(headers={"Authorization": f"Bearer {token}"}) as photon:
|
|
47
|
+
result = photon.organizations.projects.count(request)
|
|
48
|
+
print(result.count)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`countProjects` accepts an Account Service Key or an Organization Service Identity
|
|
52
|
+
API key / M2M access token. Set `PHOTON_API_TOKEN` to one of those credentials
|
|
53
|
+
and `PHOTON_ORGANIZATION_ID` to the ID of an organization it can access. See the
|
|
54
|
+
Photon dashboard and documentation to create credentials and find IDs.
|
|
55
|
+
|
|
56
|
+
## Authentication
|
|
57
|
+
|
|
58
|
+
Every credential is sent as `Authorization: Bearer <credential>`. Pass it through
|
|
59
|
+
the `headers` option. The client does not choose or check a credential type; the
|
|
60
|
+
API accepts or rejects it.
|
|
61
|
+
|
|
62
|
+
| Credential | Security scheme | Notes |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| Account Service Key (`pho_ask_...`) | `accountServiceKey` | Acts as the account and reaches every route the account can. |
|
|
65
|
+
| Project API key (`pho_sk_...`) | `projectApiKey` | Bound to one project. Accepted only under `/v1/projects/{projectId}`. |
|
|
66
|
+
| 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. |
|
|
67
|
+
| OAuth access token | `oauth2` | Scope is intersected with the permissions the route grants; an empty intersection returns `403 insufficient_scope`. |
|
|
68
|
+
|
|
69
|
+
Each operation's `security` entry in the OpenAPI contract lists the credential
|
|
70
|
+
types it accepts. A few operations accept requests without credentials.
|
|
71
|
+
|
|
72
|
+
The SDK does not run OAuth flows. Obtain OAuth access tokens and manage refresh
|
|
73
|
+
in your application's own authentication flow, then supply the current token.
|
|
74
|
+
|
|
75
|
+
`headers` accepts a mapping or a callable returning one. The callable runs before
|
|
76
|
+
every attempt, including retries, so it can return a freshly refreshed token.
|
|
77
|
+
For `AsyncPhoton` the callable may be async; for `Photon` it must be synchronous.
|
|
78
|
+
|
|
79
|
+
## Using the client
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import asyncio
|
|
83
|
+
|
|
84
|
+
from photon_api import AsyncPhoton
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def main() -> None:
|
|
88
|
+
async with AsyncPhoton(headers={"Authorization": f"Bearer {token}"}) as photon:
|
|
89
|
+
result = await photon.organizations.projects.count(request)
|
|
90
|
+
print(result.count)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `Photon` is synchronous; `AsyncPhoton` has the same methods as coroutines.
|
|
97
|
+
- Operations are grouped into namespaces that follow the API paths, for example
|
|
98
|
+
`photon.organizations.projects.count`. Namespaces and methods are snake_case,
|
|
99
|
+
as in `photon.projects.agent_profile.get`. Each method takes one input model from
|
|
100
|
+
`photon_api.rpc_generated` (for example `CountProjectsInput`) with
|
|
101
|
+
`path`, `query`, `body` and header members as the operation declares.
|
|
102
|
+
- The normal facade returns the response model. `photon.raw` has the same methods
|
|
103
|
+
and returns a `RawResponse` with `data`, `status`, `headers` and `request_id`
|
|
104
|
+
(the `x-request-id` response header when present).
|
|
105
|
+
- Close the client to release connections: use `with` / `async with`, or call
|
|
106
|
+
`close()` / `await close()`. A client you pass in with `client=` is not closed
|
|
107
|
+
for you.
|
|
108
|
+
|
|
109
|
+
Request inputs and responses are Pydantic models typed by the contract.
|
|
110
|
+
Building an input model checks types and required fields and raises
|
|
111
|
+
`pydantic.ValidationError` if they do not match; limits such as patterns,
|
|
112
|
+
lengths and ranges are checked by the API. Successful responses are parsed into
|
|
113
|
+
the model declared for their status. Fields the SDK does not know are kept, and
|
|
114
|
+
enums are open (`Literal["a", "b"] | str`), so the client keeps working when the
|
|
115
|
+
API adds fields or values. Dates and date-times are strings, as sent.
|
|
116
|
+
|
|
117
|
+
### Model fields
|
|
118
|
+
|
|
119
|
+
Omit optional fields that you do not want to send. Pass `None` only when the
|
|
120
|
+
schema permits null. Optional fields without defaults use Pydantic's `MISSING`
|
|
121
|
+
sentinel; check `model_fields_set` to see which fields were supplied. Request
|
|
122
|
+
serialization excludes omitted fields while preserving explicit nulls where
|
|
123
|
+
allowed. Extra fields are retained and sent.
|
|
124
|
+
|
|
125
|
+
The pinned Pydantic release marks `MISSING` experimental. Models containing it
|
|
126
|
+
cannot be pickled, and static type checker support is limited. JSON serialization
|
|
127
|
+
is supported; use `model_dump(mode="json", by_alias=True, exclude_unset=True)`.
|
|
128
|
+
|
|
129
|
+
## Errors
|
|
130
|
+
|
|
131
|
+
All errors extend `PhotonError`, which carries `operation_id` and `request_id`
|
|
132
|
+
when known.
|
|
133
|
+
|
|
134
|
+
| Class | Raised when | Attributes |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `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` |
|
|
137
|
+
| `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` |
|
|
138
|
+
| `TransportError` | The request failed at the HTTP layer (connection error, timeout) on the last attempt. | `operation_id`; the `httpx` error is in `__cause__` |
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from photon_api import ApiError
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
photon.organizations.projects.count(request)
|
|
145
|
+
except ApiError as error:
|
|
146
|
+
print(error.status, error.request_id, error.body)
|
|
147
|
+
raise
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Retries and timeouts
|
|
151
|
+
|
|
152
|
+
| Argument | Default | Behavior |
|
|
153
|
+
| --- | --- | --- |
|
|
154
|
+
| `timeout` | `30.0` | Seconds, applied to each attempt. |
|
|
155
|
+
| `max_attempts` | `3` | Total attempts, including the first. Values are clamped to 1 to 3; `1` disables retries. |
|
|
156
|
+
| `max_retry_after` | `60.0` | Longest `Retry-After`, in seconds, the client waits for. |
|
|
157
|
+
|
|
158
|
+
- Only `GET` operations and requests that carry an `Idempotency-Key` header
|
|
159
|
+
(from the input or from the `headers` option) are retried. Other mutations are
|
|
160
|
+
sent once.
|
|
161
|
+
- Retryable requests are retried after status 408, 429, 502, 503 or 504, or after
|
|
162
|
+
an `httpx` error (for example a connection error or timeout). The status list
|
|
163
|
+
and backoff are not configurable.
|
|
164
|
+
- Without `Retry-After`, the delay is random between 0 and
|
|
165
|
+
`min(2.0, 0.25 * 2^(attempt - 1))` seconds.
|
|
166
|
+
- `Retry-After` (seconds or an HTTP date) is used as the delay. If it exceeds
|
|
167
|
+
`max_retry_after`, the client stops retrying and handles that response (an
|
|
168
|
+
`ApiError` for an error status).
|
|
169
|
+
- When attempts run out, the last response is handled normally, or
|
|
170
|
+
`TransportError` is raised.
|
|
171
|
+
|
|
172
|
+
You can pass your own `httpx.Client` (for `Photon`) or `httpx.AsyncClient` (for
|
|
173
|
+
`AsyncPhoton`) as `client=`; the base URL, `timeout` and retry behavior above
|
|
174
|
+
still apply.
|
|
175
|
+
|
|
176
|
+
## Base URL
|
|
177
|
+
|
|
178
|
+
The client uses `https://api.photon.codes`. For tests and alternative environments,
|
|
179
|
+
set `base_url`:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
photon = Photon(
|
|
183
|
+
base_url="http://localhost:8080",
|
|
184
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## API reference
|
|
189
|
+
|
|
190
|
+
The OpenAPI contract this client is generated from is
|
|
191
|
+
[`openapi/openapi.json`](https://github.com/photon-hq/api/blob/main/openapi/openapi.json)
|
|
192
|
+
in [photon-hq/api](https://github.com/photon-hq/api), with a Postman
|
|
193
|
+
collection generated from it. The same repository contains the
|
|
194
|
+
[TypeScript](https://github.com/photon-hq/api/tree/main/packages/typescript),
|
|
195
|
+
[Python](https://github.com/photon-hq/api/tree/main/packages/python) and
|
|
196
|
+
[Rust](https://github.com/photon-hq/api/tree/main/packages/rust) clients.
|
|
197
|
+
|
|
198
|
+
## Versioning
|
|
199
|
+
|
|
200
|
+
The TypeScript, Python and Rust clients share one version and are released
|
|
201
|
+
together. Releases follow semantic versioning, starting at 0.1.0. Before 1.0,
|
|
202
|
+
breaking changes increase the minor version. Changes are listed in
|
|
203
|
+
[CHANGELOG.md](https://github.com/photon-hq/api/blob/main/CHANGELOG.md).
|
|
204
|
+
|
|
205
|
+
## Building from source
|
|
206
|
+
|
|
207
|
+
Generation requires Node.js 26 and no API credentials; Node.js is not needed to
|
|
208
|
+
use the package. From the repository root:
|
|
209
|
+
|
|
210
|
+
```sh
|
|
211
|
+
python -m pip install -r tools/python-codegen/requirements-dev.txt
|
|
212
|
+
npm ci
|
|
213
|
+
npm run regenerate:python
|
|
214
|
+
python -m pip install -e packages/python
|
|
215
|
+
npm run test:python
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
CI regenerates the client from the committed contract and fails if the output
|
|
219
|
+
differs from the committed files.
|
|
220
|
+
|
|
221
|
+
## Support and security
|
|
222
|
+
|
|
223
|
+
Report bugs and requests through
|
|
224
|
+
[GitHub issues](https://github.com/photon-hq/api/issues). Do not include
|
|
225
|
+
credentials or private data. Report vulnerabilities privately as described in
|
|
226
|
+
[SECURITY.md](https://github.com/photon-hq/api/blob/main/SECURITY.md).
|
|
227
|
+
|
|
228
|
+
This repository is generated, so external pull requests are not accepted. See
|
|
229
|
+
[CONTRIBUTING.md](https://github.com/photon-hq/api/blob/main/CONTRIBUTING.md).
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT. See [LICENSE](https://github.com/photon-hq/api/blob/main/LICENSE) and
|
|
234
|
+
[NOTICE](https://github.com/photon-hq/api/blob/main/NOTICE).
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# photonhq-api
|
|
2
|
+
|
|
3
|
+
Synchronous and asynchronous Python client for the Photon API, generated from the
|
|
4
|
+
public OpenAPI contract. Requires Python 3.11+ and imports as `photon_api`.
|
|
5
|
+
|
|
6
|
+
> **Preview release.** This version is generated from the current production
|
|
7
|
+
> contract before all of its schemas have stable names. Types the contract
|
|
8
|
+
> does not name yet have names derived from their operation (for example
|
|
9
|
+
> `ListProjectsResponse200ApplicationJson`) or carry a version prefix such as
|
|
10
|
+
> `Photon20260701_`. These type names change in 0.2.0, when the contract
|
|
11
|
+
> names them; the rename does not change requests, responses or method names.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install photonhq-api
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
from photon_api import Photon
|
|
25
|
+
from photon_api.rpc_generated import CountProjectsInput
|
|
26
|
+
|
|
27
|
+
token = os.environ["PHOTON_API_TOKEN"]
|
|
28
|
+
request = CountProjectsInput.model_validate(
|
|
29
|
+
{"path": {"organizationId": os.environ["PHOTON_ORGANIZATION_ID"]}}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
with Photon(headers={"Authorization": f"Bearer {token}"}) as photon:
|
|
33
|
+
result = photon.organizations.projects.count(request)
|
|
34
|
+
print(result.count)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`countProjects` accepts an Account Service Key or an Organization Service Identity
|
|
38
|
+
API key / M2M access token. Set `PHOTON_API_TOKEN` to one of those credentials
|
|
39
|
+
and `PHOTON_ORGANIZATION_ID` to the ID of an organization it can access. See the
|
|
40
|
+
Photon dashboard and documentation to create credentials and find IDs.
|
|
41
|
+
|
|
42
|
+
## Authentication
|
|
43
|
+
|
|
44
|
+
Every credential is sent as `Authorization: Bearer <credential>`. Pass it through
|
|
45
|
+
the `headers` option. The client does not choose or check a credential type; the
|
|
46
|
+
API accepts or rejects it.
|
|
47
|
+
|
|
48
|
+
| Credential | Security scheme | Notes |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| Account Service Key (`pho_ask_...`) | `accountServiceKey` | Acts as the account and reaches every route the account can. |
|
|
51
|
+
| Project API key (`pho_sk_...`) | `projectApiKey` | Bound to one project. Accepted only under `/v1/projects/{projectId}`. |
|
|
52
|
+
| 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. |
|
|
53
|
+
| OAuth access token | `oauth2` | Scope is intersected with the permissions the route grants; an empty intersection returns `403 insufficient_scope`. |
|
|
54
|
+
|
|
55
|
+
Each operation's `security` entry in the OpenAPI contract lists the credential
|
|
56
|
+
types it accepts. A few operations accept requests without credentials.
|
|
57
|
+
|
|
58
|
+
The SDK does not run OAuth flows. Obtain OAuth access tokens and manage refresh
|
|
59
|
+
in your application's own authentication flow, then supply the current token.
|
|
60
|
+
|
|
61
|
+
`headers` accepts a mapping or a callable returning one. The callable runs before
|
|
62
|
+
every attempt, including retries, so it can return a freshly refreshed token.
|
|
63
|
+
For `AsyncPhoton` the callable may be async; for `Photon` it must be synchronous.
|
|
64
|
+
|
|
65
|
+
## Using the client
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import asyncio
|
|
69
|
+
|
|
70
|
+
from photon_api import AsyncPhoton
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def main() -> None:
|
|
74
|
+
async with AsyncPhoton(headers={"Authorization": f"Bearer {token}"}) as photon:
|
|
75
|
+
result = await photon.organizations.projects.count(request)
|
|
76
|
+
print(result.count)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `Photon` is synchronous; `AsyncPhoton` has the same methods as coroutines.
|
|
83
|
+
- Operations are grouped into namespaces that follow the API paths, for example
|
|
84
|
+
`photon.organizations.projects.count`. Namespaces and methods are snake_case,
|
|
85
|
+
as in `photon.projects.agent_profile.get`. Each method takes one input model from
|
|
86
|
+
`photon_api.rpc_generated` (for example `CountProjectsInput`) with
|
|
87
|
+
`path`, `query`, `body` and header members as the operation declares.
|
|
88
|
+
- The normal facade returns the response model. `photon.raw` has the same methods
|
|
89
|
+
and returns a `RawResponse` with `data`, `status`, `headers` and `request_id`
|
|
90
|
+
(the `x-request-id` response header when present).
|
|
91
|
+
- Close the client to release connections: use `with` / `async with`, or call
|
|
92
|
+
`close()` / `await close()`. A client you pass in with `client=` is not closed
|
|
93
|
+
for you.
|
|
94
|
+
|
|
95
|
+
Request inputs and responses are Pydantic models typed by the contract.
|
|
96
|
+
Building an input model checks types and required fields and raises
|
|
97
|
+
`pydantic.ValidationError` if they do not match; limits such as patterns,
|
|
98
|
+
lengths and ranges are checked by the API. Successful responses are parsed into
|
|
99
|
+
the model declared for their status. Fields the SDK does not know are kept, and
|
|
100
|
+
enums are open (`Literal["a", "b"] | str`), so the client keeps working when the
|
|
101
|
+
API adds fields or values. Dates and date-times are strings, as sent.
|
|
102
|
+
|
|
103
|
+
### Model fields
|
|
104
|
+
|
|
105
|
+
Omit optional fields that you do not want to send. Pass `None` only when the
|
|
106
|
+
schema permits null. Optional fields without defaults use Pydantic's `MISSING`
|
|
107
|
+
sentinel; check `model_fields_set` to see which fields were supplied. Request
|
|
108
|
+
serialization excludes omitted fields while preserving explicit nulls where
|
|
109
|
+
allowed. Extra fields are retained and sent.
|
|
110
|
+
|
|
111
|
+
The pinned Pydantic release marks `MISSING` experimental. Models containing it
|
|
112
|
+
cannot be pickled, and static type checker support is limited. JSON serialization
|
|
113
|
+
is supported; use `model_dump(mode="json", by_alias=True, exclude_unset=True)`.
|
|
114
|
+
|
|
115
|
+
## Errors
|
|
116
|
+
|
|
117
|
+
All errors extend `PhotonError`, which carries `operation_id` and `request_id`
|
|
118
|
+
when known.
|
|
119
|
+
|
|
120
|
+
| Class | Raised when | Attributes |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `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` |
|
|
123
|
+
| `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` |
|
|
124
|
+
| `TransportError` | The request failed at the HTTP layer (connection error, timeout) on the last attempt. | `operation_id`; the `httpx` error is in `__cause__` |
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from photon_api import ApiError
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
photon.organizations.projects.count(request)
|
|
131
|
+
except ApiError as error:
|
|
132
|
+
print(error.status, error.request_id, error.body)
|
|
133
|
+
raise
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Retries and timeouts
|
|
137
|
+
|
|
138
|
+
| Argument | Default | Behavior |
|
|
139
|
+
| --- | --- | --- |
|
|
140
|
+
| `timeout` | `30.0` | Seconds, applied to each attempt. |
|
|
141
|
+
| `max_attempts` | `3` | Total attempts, including the first. Values are clamped to 1 to 3; `1` disables retries. |
|
|
142
|
+
| `max_retry_after` | `60.0` | Longest `Retry-After`, in seconds, the client waits for. |
|
|
143
|
+
|
|
144
|
+
- Only `GET` operations and requests that carry an `Idempotency-Key` header
|
|
145
|
+
(from the input or from the `headers` option) are retried. Other mutations are
|
|
146
|
+
sent once.
|
|
147
|
+
- Retryable requests are retried after status 408, 429, 502, 503 or 504, or after
|
|
148
|
+
an `httpx` error (for example a connection error or timeout). The status list
|
|
149
|
+
and backoff are not configurable.
|
|
150
|
+
- Without `Retry-After`, the delay is random between 0 and
|
|
151
|
+
`min(2.0, 0.25 * 2^(attempt - 1))` seconds.
|
|
152
|
+
- `Retry-After` (seconds or an HTTP date) is used as the delay. If it exceeds
|
|
153
|
+
`max_retry_after`, the client stops retrying and handles that response (an
|
|
154
|
+
`ApiError` for an error status).
|
|
155
|
+
- When attempts run out, the last response is handled normally, or
|
|
156
|
+
`TransportError` is raised.
|
|
157
|
+
|
|
158
|
+
You can pass your own `httpx.Client` (for `Photon`) or `httpx.AsyncClient` (for
|
|
159
|
+
`AsyncPhoton`) as `client=`; the base URL, `timeout` and retry behavior above
|
|
160
|
+
still apply.
|
|
161
|
+
|
|
162
|
+
## Base URL
|
|
163
|
+
|
|
164
|
+
The client uses `https://api.photon.codes`. For tests and alternative environments,
|
|
165
|
+
set `base_url`:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
photon = Photon(
|
|
169
|
+
base_url="http://localhost:8080",
|
|
170
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## API reference
|
|
175
|
+
|
|
176
|
+
The OpenAPI contract this client is generated from is
|
|
177
|
+
[`openapi/openapi.json`](https://github.com/photon-hq/api/blob/main/openapi/openapi.json)
|
|
178
|
+
in [photon-hq/api](https://github.com/photon-hq/api), with a Postman
|
|
179
|
+
collection generated from it. The same repository contains the
|
|
180
|
+
[TypeScript](https://github.com/photon-hq/api/tree/main/packages/typescript),
|
|
181
|
+
[Python](https://github.com/photon-hq/api/tree/main/packages/python) and
|
|
182
|
+
[Rust](https://github.com/photon-hq/api/tree/main/packages/rust) clients.
|
|
183
|
+
|
|
184
|
+
## Versioning
|
|
185
|
+
|
|
186
|
+
The TypeScript, Python and Rust clients share one version and are released
|
|
187
|
+
together. Releases follow semantic versioning, starting at 0.1.0. Before 1.0,
|
|
188
|
+
breaking changes increase the minor version. Changes are listed in
|
|
189
|
+
[CHANGELOG.md](https://github.com/photon-hq/api/blob/main/CHANGELOG.md).
|
|
190
|
+
|
|
191
|
+
## Building from source
|
|
192
|
+
|
|
193
|
+
Generation requires Node.js 26 and no API credentials; Node.js is not needed to
|
|
194
|
+
use the package. From the repository root:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
python -m pip install -r tools/python-codegen/requirements-dev.txt
|
|
198
|
+
npm ci
|
|
199
|
+
npm run regenerate:python
|
|
200
|
+
python -m pip install -e packages/python
|
|
201
|
+
npm run test:python
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
CI regenerates the client from the committed contract and fails if the output
|
|
205
|
+
differs from the committed files.
|
|
206
|
+
|
|
207
|
+
## Support and security
|
|
208
|
+
|
|
209
|
+
Report bugs and requests through
|
|
210
|
+
[GitHub issues](https://github.com/photon-hq/api/issues). Do not include
|
|
211
|
+
credentials or private data. Report vulnerabilities privately as described in
|
|
212
|
+
[SECURITY.md](https://github.com/photon-hq/api/blob/main/SECURITY.md).
|
|
213
|
+
|
|
214
|
+
This repository is generated, so external pull requests are not accepted. See
|
|
215
|
+
[CONTRIBUTING.md](https://github.com/photon-hq/api/blob/main/CONTRIBUTING.md).
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT. See [LICENSE](https://github.com/photon-hq/api/blob/main/LICENSE) and
|
|
220
|
+
[NOTICE](https://github.com/photon-hq/api/blob/main/NOTICE).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.31.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "photonhq-api"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Generated Photon API RPC client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Photon" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"email-validator>=2.3.0,<3",
|
|
15
|
+
"httpx>=0.28.1,<1",
|
|
16
|
+
# Generated models use Pydantic's experimental MISSING sentinel, which may
|
|
17
|
+
# change in a minor release.
|
|
18
|
+
"pydantic>=2.13.5,<2.14",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Repository = "https://github.com/photon-hq/api"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/photon_api"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
addopts = "-q"
|
|
30
|
+
|
|
31
|
+
[tool.ruff]
|
|
32
|
+
target-version = "py311"
|
|
33
|
+
line-length = 100
|
|
34
|
+
|
|
35
|
+
[tool.ruff.lint]
|
|
36
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff.lint.per-file-ignores]
|
|
39
|
+
"src/photon_api/generated/__init__.py" = ["F403"]
|
|
40
|
+
"src/photon_api/generated/models.py" = ["E501", "UP007", "UP037", "UP045"]
|
|
41
|
+
# Preserve authored OpenAPI prose in generated method docstrings.
|
|
42
|
+
"src/photon_api/rpc_generated.py" = ["E501"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .client import AsyncPhoton, Photon
|
|
2
|
+
from .errors import (
|
|
3
|
+
ApiError,
|
|
4
|
+
PhotonError,
|
|
5
|
+
ResponseValidationError,
|
|
6
|
+
TransportError,
|
|
7
|
+
)
|
|
8
|
+
from .transport import RawResponse
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ApiError",
|
|
12
|
+
"AsyncPhoton",
|
|
13
|
+
"Photon",
|
|
14
|
+
"PhotonError",
|
|
15
|
+
"RawResponse",
|
|
16
|
+
"ResponseValidationError",
|
|
17
|
+
"TransportError",
|
|
18
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Base classes and helpers of the generated models (tools/python-codegen).
|
|
2
|
+
|
|
3
|
+
The models carry the contract's types; values the contract restricts only by
|
|
4
|
+
validation keywords (patterns, lengths, bounds, sizes) are left to the service.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Annotated, Any, Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
from pydantic import AfterValidator, ConfigDict, TypeAdapter
|
|
10
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
11
|
+
from pydantic import RootModel as PydanticRootModel
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseModel(PydanticBaseModel):
|
|
17
|
+
# Members the SDK does not know are kept (a response may gain fields).
|
|
18
|
+
model_config = ConfigDict(extra="allow", defer_build=True)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RootModel(PydanticRootModel[T], Generic[T]):
|
|
22
|
+
model_config = ConfigDict(defer_build=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_all_of(data: Any, *constraints: type[PydanticBaseModel]) -> Any:
|
|
26
|
+
"""Check the input of a union narrowed by object constraints against each constraint.
|
|
27
|
+
|
|
28
|
+
Generated (tools/python-codegen/intersections.py) as a `mode="before"`
|
|
29
|
+
validator: every constraint model validates the value as given, and the
|
|
30
|
+
root type validates the union; the value is valid only if all accept it.
|
|
31
|
+
A model instance is checked through its JSON form.
|
|
32
|
+
"""
|
|
33
|
+
raw = (
|
|
34
|
+
data.model_dump(mode="json", by_alias=True) if isinstance(data, PydanticBaseModel) else data
|
|
35
|
+
)
|
|
36
|
+
for constraint in constraints:
|
|
37
|
+
constraint.model_validate(raw)
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PrefixItems:
|
|
42
|
+
"""`PrefixItems[T1, ..., Tn]`: a JSON Schema `prefixItems` array without `items`.
|
|
43
|
+
|
|
44
|
+
Item i, when present, validates as Ti; later items are unconstrained, and
|
|
45
|
+
the array may be shorter than n. Generated for arrays whose positional
|
|
46
|
+
items the model generator would otherwise drop
|
|
47
|
+
(tools/python-codegen/prefix_items.py). The value is a `list`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __class_getitem__(cls, params: Any) -> Any:
|
|
51
|
+
types = params if isinstance(params, tuple) else (params,)
|
|
52
|
+
adapters: list[TypeAdapter[Any]] = []
|
|
53
|
+
|
|
54
|
+
def validate(value: list[Any]) -> list[Any]:
|
|
55
|
+
if not adapters:
|
|
56
|
+
adapters.extend(TypeAdapter(item) for item in types)
|
|
57
|
+
return [
|
|
58
|
+
adapters[index].validate_python(item) if index < len(adapters) else item
|
|
59
|
+
for index, item in enumerate(value)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
return Annotated[list[Any], AfterValidator(validate)]
|