firetail-finchly 0.0.1__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.
- firetail_finchly-0.0.1/.github/workflows/ci.yml +27 -0
- firetail_finchly-0.0.1/.github/workflows/release.yml +53 -0
- firetail_finchly-0.0.1/.gitignore +11 -0
- firetail_finchly-0.0.1/CHANGELOG.md +27 -0
- firetail_finchly-0.0.1/PKG-INFO +242 -0
- firetail_finchly-0.0.1/README.md +222 -0
- firetail_finchly-0.0.1/pyproject.toml +38 -0
- firetail_finchly-0.0.1/src/firetail_finchly/__init__.py +37 -0
- firetail_finchly-0.0.1/src/firetail_finchly/_version.py +1 -0
- firetail_finchly-0.0.1/src/firetail_finchly/config.py +90 -0
- firetail_finchly-0.0.1/src/firetail_finchly/errors.py +48 -0
- firetail_finchly-0.0.1/src/firetail_finchly/identity.py +322 -0
- firetail_finchly-0.0.1/src/firetail_finchly/manifest.py +97 -0
- firetail_finchly-0.0.1/src/firetail_finchly/platform_api.py +24 -0
- firetail_finchly-0.0.1/src/firetail_finchly/py.typed +0 -0
- firetail_finchly-0.0.1/src/firetail_finchly/service.py +351 -0
- firetail_finchly-0.0.1/tests/conformance/README.md +4 -0
- firetail_finchly-0.0.1/tests/conformance/identity-token.v1.json +316 -0
- firetail_finchly-0.0.1/tests/test_config.py +94 -0
- firetail_finchly-0.0.1/tests/test_identity.py +293 -0
- firetail_finchly-0.0.1/tests/test_manifest.py +117 -0
- firetail_finchly-0.0.1/tests/test_service.py +452 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, dev]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python }}
|
|
22
|
+
cache: pip
|
|
23
|
+
cache-dependency-path: pyproject.toml
|
|
24
|
+
- run: pip install -e ".[dev]"
|
|
25
|
+
- run: python -m pytest
|
|
26
|
+
- name: Check the package builds
|
|
27
|
+
run: python -m build
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes firetail_finchly when a version tag is pushed.
|
|
4
|
+
# Uses PyPI trusted publishing: no PyPI token is stored. See README, "Release".
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Check the tag matches the package version
|
|
23
|
+
env:
|
|
24
|
+
TAG: ${{ github.ref_name }}
|
|
25
|
+
run: |
|
|
26
|
+
version="$(python -c "exec(open('src/firetail_finchly/_version.py').read()); print(__version__)")"
|
|
27
|
+
if [ "v${version}" != "${TAG}" ]; then
|
|
28
|
+
echo "Tag ${TAG} does not match package version ${version}" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
- run: pip install -e ".[dev]"
|
|
33
|
+
- run: python -m pytest
|
|
34
|
+
- run: python -m build
|
|
35
|
+
|
|
36
|
+
- uses: actions/upload-artifact@v4
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
if-no-files-found: error
|
|
41
|
+
|
|
42
|
+
publish:
|
|
43
|
+
needs: build
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
environment: pypi
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/download-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `firetail_finchly`. Versions follow semver.
|
|
4
|
+
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.0.1 - 2026-11-11
|
|
8
|
+
|
|
9
|
+
The first release.
|
|
10
|
+
|
|
11
|
+
- Package skeleton under its final name.
|
|
12
|
+
- `load_config`: read and validate the `PLATFORM_*` settings.
|
|
13
|
+
- `load_manifest`, `check_manifest`: load a manifest and check the fields
|
|
14
|
+
the SDK depends on.
|
|
15
|
+
- `PLATFORM_API_VERSION`, `SUPPORTED_PLATFORM_API_VERSIONS`, `SDK_NAME`,
|
|
16
|
+
`SDK_VERSION`.
|
|
17
|
+
- `IdentityVerifier`: verify the `x-platform-identity` token against the
|
|
18
|
+
gateway's published keys. Rejections are an `IdentityError` with a
|
|
19
|
+
`reason` from the specification. Tested against the identity token
|
|
20
|
+
conformance vectors.
|
|
21
|
+
- New dependency: `PyJWT[crypto]`.
|
|
22
|
+
- `ServiceClient`: call another service through the gateway, as the
|
|
23
|
+
service itself or for a user. Makes the instance key, registers it with
|
|
24
|
+
the service credential, and extends it before it expires. `url` and
|
|
25
|
+
`headers` let another HTTP client make the call.
|
|
26
|
+
- `load_config` reads `PLATFORM_SERVICE_CREDENTIAL`.
|
|
27
|
+
- New error codes: `service_call_invalid`, `service_key_rejected`.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: firetail_finchly
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python SDK for the Finchly platform
|
|
5
|
+
Project-URL: Repository, https://github.com/firetail-io/finchly-sdk-py
|
|
6
|
+
Project-URL: Changelog, https://github.com/firetail-io/finchly-sdk-py/blob/main/CHANGELOG.md
|
|
7
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: pyjwt[crypto]<3,>=2.8
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# firetail_finchly
|
|
22
|
+
|
|
23
|
+
Python SDK for the [Finchly](https://github.com/firetail-io/finchly)
|
|
24
|
+
platform. A service or app uses it to register with a Finchly deployment,
|
|
25
|
+
verify who is calling it, and reach the platform's database, object store,
|
|
26
|
+
queue and other services.
|
|
27
|
+
|
|
28
|
+
Status: early. The package is tested, but only the first four modules
|
|
29
|
+
exist.
|
|
30
|
+
|
|
31
|
+
The SDK implements the platform API, which is specified in
|
|
32
|
+
`firetail-io/finchly` under `spec/`. This release speaks platform API `v1`
|
|
33
|
+
(draft).
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pip install firetail_finchly
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10 or later. Depends on PyJWT with its `crypto` extra.
|
|
42
|
+
|
|
43
|
+
## Use
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from firetail_finchly import load_config, load_manifest
|
|
47
|
+
|
|
48
|
+
config = load_config() # reads PLATFORM_* settings
|
|
49
|
+
manifest = load_manifest(config.manifest_path)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Both raise a `FinchlyError` with a stable `code` and a `problems` tuple that
|
|
53
|
+
names everything wrong, so a misconfigured container fails once with the
|
|
54
|
+
full picture.
|
|
55
|
+
|
|
56
|
+
### Who is calling
|
|
57
|
+
|
|
58
|
+
The gateway puts a short-lived signed token in the `x-platform-identity`
|
|
59
|
+
header of every request it proxies. It is the only identity a service
|
|
60
|
+
should accept.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from firetail_finchly import IdentityError, IdentityVerifier
|
|
64
|
+
|
|
65
|
+
# Once, at startup. It caches the gateway's issuer and signing keys, and
|
|
66
|
+
# is safe to share between threads.
|
|
67
|
+
identity = IdentityVerifier(slug=manifest["slug"], gateway_url=config.gateway_url)
|
|
68
|
+
|
|
69
|
+
# Per request.
|
|
70
|
+
try:
|
|
71
|
+
caller = identity.from_headers(request.headers)
|
|
72
|
+
caller.subject # user id, or "service:<slug>"
|
|
73
|
+
caller.email # None for a service calling as itself
|
|
74
|
+
caller.groups # platform groups
|
|
75
|
+
caller.roles # roles for this service: "user", "operator"
|
|
76
|
+
caller.actor # "service:<slug>" when a service acts for the user
|
|
77
|
+
except IdentityError as err:
|
|
78
|
+
... # Unauthenticated: respond 401. err.reason is for logs only.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Any other `FinchlyError` from the verifier means the gateway could not be
|
|
82
|
+
reached; respond 503.
|
|
83
|
+
|
|
84
|
+
A token is rejected unless it is signed with ES256 by a key the gateway
|
|
85
|
+
publishes, names that gateway as issuer and this service as its only
|
|
86
|
+
audience, and has not expired. The rules are in `spec/identity-token.md` in
|
|
87
|
+
`firetail-io/finchly`.
|
|
88
|
+
|
|
89
|
+
No other header is ever consulted. On a path the manifest declares in
|
|
90
|
+
`publicPaths` there is no token, and the service authenticates the request
|
|
91
|
+
itself.
|
|
92
|
+
|
|
93
|
+
### Calling another service
|
|
94
|
+
|
|
95
|
+
Services never address each other directly. A call goes through the
|
|
96
|
+
gateway, signed by this instance's own key.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from firetail_finchly import ServiceClient
|
|
100
|
+
|
|
101
|
+
# Once, at startup. Safe to share between threads.
|
|
102
|
+
service = ServiceClient(
|
|
103
|
+
slug=manifest["slug"],
|
|
104
|
+
gateway_url=config.gateway_url,
|
|
105
|
+
credential=config.service_credential,
|
|
106
|
+
)
|
|
107
|
+
service.start() # optional: fail now, not on the first call
|
|
108
|
+
|
|
109
|
+
# As the service itself.
|
|
110
|
+
response = service.call(
|
|
111
|
+
"notification-service",
|
|
112
|
+
"/api/send",
|
|
113
|
+
method="POST",
|
|
114
|
+
headers={"Content-Type": "application/json"},
|
|
115
|
+
body=b'{"text": "hello"}',
|
|
116
|
+
)
|
|
117
|
+
response.status, response.headers, response.body, response.json()
|
|
118
|
+
|
|
119
|
+
# For a user: the target sees that user, and this service acting for them.
|
|
120
|
+
service.call("notification-service", "/api/inbox", as_user=caller.subject)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`call` returns the response, whatever its status.
|
|
124
|
+
|
|
125
|
+
- **403** means this service's manifest does not list the target in
|
|
126
|
+
`calls`, or the user has no access to the target.
|
|
127
|
+
- **A redirect is handed back, not followed.**
|
|
128
|
+
- **Nothing is retried.** After a 401 the next call registers the key
|
|
129
|
+
again, in case the gateway no longer knows it.
|
|
130
|
+
|
|
131
|
+
To use another HTTP client, such as `requests` or `httpx`, take the address
|
|
132
|
+
and the header from the client and send the request yourself. A token is
|
|
133
|
+
for one target and is valid for a minute, so get fresh headers each time:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
requests.post(
|
|
137
|
+
service.url("notification-service", "/api/send"),
|
|
138
|
+
headers=service.headers("notification-service"),
|
|
139
|
+
json={"text": "hello"},
|
|
140
|
+
allow_redirects=False,
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The instance makes an ES256 key pair in memory the first time it is needed.
|
|
145
|
+
The private half never leaves the process. The public half is registered
|
|
146
|
+
with the gateway using the credential the service was deployed with, and
|
|
147
|
+
extended by itself for as long as the instance runs. The rules are in
|
|
148
|
+
`spec/service-token.md` and `spec/registration.md` in `firetail-io/finchly`.
|
|
149
|
+
|
|
150
|
+
Only a built-in service has a credential. An app's key is accepted when it
|
|
151
|
+
registers, which the `app` module will do; until then an app cannot use
|
|
152
|
+
`service`.
|
|
153
|
+
|
|
154
|
+
### Settings
|
|
155
|
+
|
|
156
|
+
| Variable | Required | Meaning |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| `PLATFORM_GATEWAY_URL` | yes | The gateway's internal address |
|
|
159
|
+
| `PLATFORM_MANIFEST_PATH` | no | Manifest path in the container. Default `/finchly/manifest.json` |
|
|
160
|
+
| `PLATFORM_APP_ADDRESS` | no | Address the gateway should use to reach this service |
|
|
161
|
+
| `PLATFORM_ENROLMENT_TOKEN` | no | Token for self-registration |
|
|
162
|
+
| `PLATFORM_SERVICE_CREDENTIAL` | no | What a built-in service registers its instance key with |
|
|
163
|
+
|
|
164
|
+
The enrolment token and the service credential are left out of the
|
|
165
|
+
config's `repr`, so logging the config does not leak them.
|
|
166
|
+
|
|
167
|
+
## Modules
|
|
168
|
+
|
|
169
|
+
The Python SDK may trail the Node SDK for modules that only Node services
|
|
170
|
+
use.
|
|
171
|
+
|
|
172
|
+
| Module | What it does | State |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `config` | Read and validate the platform settings | Done |
|
|
175
|
+
| `manifest` | Load the manifest and check what the SDK depends on | Done |
|
|
176
|
+
| `identity` | Verify `x-platform-identity` | Done. Directory lookups (`list_users`, `members_of`) and `require_*` helpers are planned |
|
|
177
|
+
| `app` | Register with the platform, serve `/_platform/*`, renew keys, fetch settings | Next |
|
|
178
|
+
| `service` | Call other services through the gateway, as the service or for a user | Done for built-in services. An app's first key waits on `app` |
|
|
179
|
+
| `db` | PostgreSQL: queries, transactions, advisory locks, migrations | Planned |
|
|
180
|
+
| `blobs` | Object storage with conditional writes | Planned |
|
|
181
|
+
| `queue` | Publish, lease, ack, nack | Planned |
|
|
182
|
+
| `secrets` | Secrets and envelope encryption | Planned |
|
|
183
|
+
| `llm` | Model calls through the platform | Planned |
|
|
184
|
+
| `audit`, `usage`, `notify`, `schedule`, `connectors` | Clients for platform services | Planned |
|
|
185
|
+
| `ui` | Shared header, footer and theme | Not planned; Node only |
|
|
186
|
+
|
|
187
|
+
The manifest check here is not the full schema. The platform runs the full
|
|
188
|
+
check at registration and is the authority.
|
|
189
|
+
|
|
190
|
+
## Develop
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
python -m venv .venv && . .venv/bin/activate
|
|
194
|
+
pip install -e ".[dev]"
|
|
195
|
+
python -m pytest
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Without a local Python environment, run them in a container:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
docker run --rm -v "$PWD":/src:ro python:3.12-slim sh -c \
|
|
202
|
+
"cp -r /src /w && cd /w && pip install -q -e '.[dev]' && python -m pytest -q"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Conformance vectors
|
|
206
|
+
|
|
207
|
+
`tests/conformance/` holds test vectors copied from `firetail-io/finchly`
|
|
208
|
+
(`conformance/vectors/`). Both SDKs must reach the verdict each vector
|
|
209
|
+
states. Do not edit them here: change them in `finchly`, regenerate, and
|
|
210
|
+
copy the file to both SDK repositories. Until `finchly` publishes its
|
|
211
|
+
conformance suite as an image, the copy is made by hand.
|
|
212
|
+
|
|
213
|
+
## Release
|
|
214
|
+
|
|
215
|
+
Each package version follows semver, independent of the Node SDK and of the
|
|
216
|
+
platform.
|
|
217
|
+
|
|
218
|
+
1. Changes land on `main` by pull request. Each one adds a line to
|
|
219
|
+
`CHANGELOG.md` under "Unreleased".
|
|
220
|
+
2. To release, set the version in `src/firetail_finchly/_version.py`, move
|
|
221
|
+
the changelog entries under that version, merge, and tag the merge
|
|
222
|
+
commit `v` plus the version, for example `v1.2.0`.
|
|
223
|
+
3. The `release` workflow checks the tag matches the version, runs the
|
|
224
|
+
tests, builds the sdist and wheel, and publishes.
|
|
225
|
+
4. A release candidate uses the PEP 440 form, `1.2.0rc1`, tagged
|
|
226
|
+
`v1.2.0rc1`. pip does not install pre-releases unless asked.
|
|
227
|
+
5. A bad release is yanked on PyPI and followed by a patch release.
|
|
228
|
+
|
|
229
|
+
Publishing uses PyPI trusted publishing: the workflow proves its identity to
|
|
230
|
+
PyPI with its GitHub Actions token, so no PyPI token is stored in this
|
|
231
|
+
repository.
|
|
232
|
+
|
|
233
|
+
One-time setup, not yet done:
|
|
234
|
+
|
|
235
|
+
- On PyPI, add a pending trusted publisher for the project
|
|
236
|
+
`firetail_finchly`: owner `firetail-io`, repository `finchly-sdk-py`,
|
|
237
|
+
workflow `release.yml`, environment `pypi`.
|
|
238
|
+
- Create a GitHub environment called `pypi` in this repository, restricted
|
|
239
|
+
to tags.
|
|
240
|
+
|
|
241
|
+
Still to add: the conformance suite from `firetail-io/finchly`, run in CI on
|
|
242
|
+
every change, once that suite exists.
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# firetail_finchly
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Finchly](https://github.com/firetail-io/finchly)
|
|
4
|
+
platform. A service or app uses it to register with a Finchly deployment,
|
|
5
|
+
verify who is calling it, and reach the platform's database, object store,
|
|
6
|
+
queue and other services.
|
|
7
|
+
|
|
8
|
+
Status: early. The package is tested, but only the first four modules
|
|
9
|
+
exist.
|
|
10
|
+
|
|
11
|
+
The SDK implements the platform API, which is specified in
|
|
12
|
+
`firetail-io/finchly` under `spec/`. This release speaks platform API `v1`
|
|
13
|
+
(draft).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
pip install firetail_finchly
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.10 or later. Depends on PyJWT with its `crypto` extra.
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from firetail_finchly import load_config, load_manifest
|
|
27
|
+
|
|
28
|
+
config = load_config() # reads PLATFORM_* settings
|
|
29
|
+
manifest = load_manifest(config.manifest_path)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Both raise a `FinchlyError` with a stable `code` and a `problems` tuple that
|
|
33
|
+
names everything wrong, so a misconfigured container fails once with the
|
|
34
|
+
full picture.
|
|
35
|
+
|
|
36
|
+
### Who is calling
|
|
37
|
+
|
|
38
|
+
The gateway puts a short-lived signed token in the `x-platform-identity`
|
|
39
|
+
header of every request it proxies. It is the only identity a service
|
|
40
|
+
should accept.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from firetail_finchly import IdentityError, IdentityVerifier
|
|
44
|
+
|
|
45
|
+
# Once, at startup. It caches the gateway's issuer and signing keys, and
|
|
46
|
+
# is safe to share between threads.
|
|
47
|
+
identity = IdentityVerifier(slug=manifest["slug"], gateway_url=config.gateway_url)
|
|
48
|
+
|
|
49
|
+
# Per request.
|
|
50
|
+
try:
|
|
51
|
+
caller = identity.from_headers(request.headers)
|
|
52
|
+
caller.subject # user id, or "service:<slug>"
|
|
53
|
+
caller.email # None for a service calling as itself
|
|
54
|
+
caller.groups # platform groups
|
|
55
|
+
caller.roles # roles for this service: "user", "operator"
|
|
56
|
+
caller.actor # "service:<slug>" when a service acts for the user
|
|
57
|
+
except IdentityError as err:
|
|
58
|
+
... # Unauthenticated: respond 401. err.reason is for logs only.
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Any other `FinchlyError` from the verifier means the gateway could not be
|
|
62
|
+
reached; respond 503.
|
|
63
|
+
|
|
64
|
+
A token is rejected unless it is signed with ES256 by a key the gateway
|
|
65
|
+
publishes, names that gateway as issuer and this service as its only
|
|
66
|
+
audience, and has not expired. The rules are in `spec/identity-token.md` in
|
|
67
|
+
`firetail-io/finchly`.
|
|
68
|
+
|
|
69
|
+
No other header is ever consulted. On a path the manifest declares in
|
|
70
|
+
`publicPaths` there is no token, and the service authenticates the request
|
|
71
|
+
itself.
|
|
72
|
+
|
|
73
|
+
### Calling another service
|
|
74
|
+
|
|
75
|
+
Services never address each other directly. A call goes through the
|
|
76
|
+
gateway, signed by this instance's own key.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from firetail_finchly import ServiceClient
|
|
80
|
+
|
|
81
|
+
# Once, at startup. Safe to share between threads.
|
|
82
|
+
service = ServiceClient(
|
|
83
|
+
slug=manifest["slug"],
|
|
84
|
+
gateway_url=config.gateway_url,
|
|
85
|
+
credential=config.service_credential,
|
|
86
|
+
)
|
|
87
|
+
service.start() # optional: fail now, not on the first call
|
|
88
|
+
|
|
89
|
+
# As the service itself.
|
|
90
|
+
response = service.call(
|
|
91
|
+
"notification-service",
|
|
92
|
+
"/api/send",
|
|
93
|
+
method="POST",
|
|
94
|
+
headers={"Content-Type": "application/json"},
|
|
95
|
+
body=b'{"text": "hello"}',
|
|
96
|
+
)
|
|
97
|
+
response.status, response.headers, response.body, response.json()
|
|
98
|
+
|
|
99
|
+
# For a user: the target sees that user, and this service acting for them.
|
|
100
|
+
service.call("notification-service", "/api/inbox", as_user=caller.subject)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`call` returns the response, whatever its status.
|
|
104
|
+
|
|
105
|
+
- **403** means this service's manifest does not list the target in
|
|
106
|
+
`calls`, or the user has no access to the target.
|
|
107
|
+
- **A redirect is handed back, not followed.**
|
|
108
|
+
- **Nothing is retried.** After a 401 the next call registers the key
|
|
109
|
+
again, in case the gateway no longer knows it.
|
|
110
|
+
|
|
111
|
+
To use another HTTP client, such as `requests` or `httpx`, take the address
|
|
112
|
+
and the header from the client and send the request yourself. A token is
|
|
113
|
+
for one target and is valid for a minute, so get fresh headers each time:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
requests.post(
|
|
117
|
+
service.url("notification-service", "/api/send"),
|
|
118
|
+
headers=service.headers("notification-service"),
|
|
119
|
+
json={"text": "hello"},
|
|
120
|
+
allow_redirects=False,
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The instance makes an ES256 key pair in memory the first time it is needed.
|
|
125
|
+
The private half never leaves the process. The public half is registered
|
|
126
|
+
with the gateway using the credential the service was deployed with, and
|
|
127
|
+
extended by itself for as long as the instance runs. The rules are in
|
|
128
|
+
`spec/service-token.md` and `spec/registration.md` in `firetail-io/finchly`.
|
|
129
|
+
|
|
130
|
+
Only a built-in service has a credential. An app's key is accepted when it
|
|
131
|
+
registers, which the `app` module will do; until then an app cannot use
|
|
132
|
+
`service`.
|
|
133
|
+
|
|
134
|
+
### Settings
|
|
135
|
+
|
|
136
|
+
| Variable | Required | Meaning |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `PLATFORM_GATEWAY_URL` | yes | The gateway's internal address |
|
|
139
|
+
| `PLATFORM_MANIFEST_PATH` | no | Manifest path in the container. Default `/finchly/manifest.json` |
|
|
140
|
+
| `PLATFORM_APP_ADDRESS` | no | Address the gateway should use to reach this service |
|
|
141
|
+
| `PLATFORM_ENROLMENT_TOKEN` | no | Token for self-registration |
|
|
142
|
+
| `PLATFORM_SERVICE_CREDENTIAL` | no | What a built-in service registers its instance key with |
|
|
143
|
+
|
|
144
|
+
The enrolment token and the service credential are left out of the
|
|
145
|
+
config's `repr`, so logging the config does not leak them.
|
|
146
|
+
|
|
147
|
+
## Modules
|
|
148
|
+
|
|
149
|
+
The Python SDK may trail the Node SDK for modules that only Node services
|
|
150
|
+
use.
|
|
151
|
+
|
|
152
|
+
| Module | What it does | State |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `config` | Read and validate the platform settings | Done |
|
|
155
|
+
| `manifest` | Load the manifest and check what the SDK depends on | Done |
|
|
156
|
+
| `identity` | Verify `x-platform-identity` | Done. Directory lookups (`list_users`, `members_of`) and `require_*` helpers are planned |
|
|
157
|
+
| `app` | Register with the platform, serve `/_platform/*`, renew keys, fetch settings | Next |
|
|
158
|
+
| `service` | Call other services through the gateway, as the service or for a user | Done for built-in services. An app's first key waits on `app` |
|
|
159
|
+
| `db` | PostgreSQL: queries, transactions, advisory locks, migrations | Planned |
|
|
160
|
+
| `blobs` | Object storage with conditional writes | Planned |
|
|
161
|
+
| `queue` | Publish, lease, ack, nack | Planned |
|
|
162
|
+
| `secrets` | Secrets and envelope encryption | Planned |
|
|
163
|
+
| `llm` | Model calls through the platform | Planned |
|
|
164
|
+
| `audit`, `usage`, `notify`, `schedule`, `connectors` | Clients for platform services | Planned |
|
|
165
|
+
| `ui` | Shared header, footer and theme | Not planned; Node only |
|
|
166
|
+
|
|
167
|
+
The manifest check here is not the full schema. The platform runs the full
|
|
168
|
+
check at registration and is the authority.
|
|
169
|
+
|
|
170
|
+
## Develop
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
python -m venv .venv && . .venv/bin/activate
|
|
174
|
+
pip install -e ".[dev]"
|
|
175
|
+
python -m pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Without a local Python environment, run them in a container:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
docker run --rm -v "$PWD":/src:ro python:3.12-slim sh -c \
|
|
182
|
+
"cp -r /src /w && cd /w && pip install -q -e '.[dev]' && python -m pytest -q"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Conformance vectors
|
|
186
|
+
|
|
187
|
+
`tests/conformance/` holds test vectors copied from `firetail-io/finchly`
|
|
188
|
+
(`conformance/vectors/`). Both SDKs must reach the verdict each vector
|
|
189
|
+
states. Do not edit them here: change them in `finchly`, regenerate, and
|
|
190
|
+
copy the file to both SDK repositories. Until `finchly` publishes its
|
|
191
|
+
conformance suite as an image, the copy is made by hand.
|
|
192
|
+
|
|
193
|
+
## Release
|
|
194
|
+
|
|
195
|
+
Each package version follows semver, independent of the Node SDK and of the
|
|
196
|
+
platform.
|
|
197
|
+
|
|
198
|
+
1. Changes land on `main` by pull request. Each one adds a line to
|
|
199
|
+
`CHANGELOG.md` under "Unreleased".
|
|
200
|
+
2. To release, set the version in `src/firetail_finchly/_version.py`, move
|
|
201
|
+
the changelog entries under that version, merge, and tag the merge
|
|
202
|
+
commit `v` plus the version, for example `v1.2.0`.
|
|
203
|
+
3. The `release` workflow checks the tag matches the version, runs the
|
|
204
|
+
tests, builds the sdist and wheel, and publishes.
|
|
205
|
+
4. A release candidate uses the PEP 440 form, `1.2.0rc1`, tagged
|
|
206
|
+
`v1.2.0rc1`. pip does not install pre-releases unless asked.
|
|
207
|
+
5. A bad release is yanked on PyPI and followed by a patch release.
|
|
208
|
+
|
|
209
|
+
Publishing uses PyPI trusted publishing: the workflow proves its identity to
|
|
210
|
+
PyPI with its GitHub Actions token, so no PyPI token is stored in this
|
|
211
|
+
repository.
|
|
212
|
+
|
|
213
|
+
One-time setup, not yet done:
|
|
214
|
+
|
|
215
|
+
- On PyPI, add a pending trusted publisher for the project
|
|
216
|
+
`firetail_finchly`: owner `firetail-io`, repository `finchly-sdk-py`,
|
|
217
|
+
workflow `release.yml`, environment `pypi`.
|
|
218
|
+
- Create a GitHub environment called `pypi` in this repository, restricted
|
|
219
|
+
to tags.
|
|
220
|
+
|
|
221
|
+
Still to add: the conformance suite from `firetail-io/finchly`, run in CI on
|
|
222
|
+
every change, once that suite exists.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "firetail_finchly"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python SDK for the Finchly platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"PyJWT[crypto]>=2.8,<3",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 2 - Pre-Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/firetail-io/finchly-sdk-py"
|
|
26
|
+
Changelog = "https://github.com/firetail-io/finchly-sdk-py/blob/main/CHANGELOG.md"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["pytest>=8", "build>=1.2"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.version]
|
|
32
|
+
path = "src/firetail_finchly/_version.py"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/firetail_finchly"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Python SDK for the Finchly platform."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .config import DEFAULT_MANIFEST_PATH, PlatformConfig, load_config
|
|
5
|
+
from .errors import IDENTITY_REJECTIONS, FinchlyError, IdentityError
|
|
6
|
+
from .identity import IDENTITY_HEADER, Identity, IdentityVerifier
|
|
7
|
+
from .manifest import check_manifest, load_manifest
|
|
8
|
+
from .platform_api import (
|
|
9
|
+
MANIFEST_VERSION,
|
|
10
|
+
PLATFORM_API_VERSION,
|
|
11
|
+
SDK_NAME,
|
|
12
|
+
SDK_VERSION,
|
|
13
|
+
SUPPORTED_PLATFORM_API_VERSIONS,
|
|
14
|
+
)
|
|
15
|
+
from .service import ServiceClient, ServiceResponse
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"DEFAULT_MANIFEST_PATH",
|
|
19
|
+
"FinchlyError",
|
|
20
|
+
"IDENTITY_HEADER",
|
|
21
|
+
"IDENTITY_REJECTIONS",
|
|
22
|
+
"Identity",
|
|
23
|
+
"IdentityError",
|
|
24
|
+
"IdentityVerifier",
|
|
25
|
+
"MANIFEST_VERSION",
|
|
26
|
+
"PLATFORM_API_VERSION",
|
|
27
|
+
"PlatformConfig",
|
|
28
|
+
"SDK_NAME",
|
|
29
|
+
"SDK_VERSION",
|
|
30
|
+
"SUPPORTED_PLATFORM_API_VERSIONS",
|
|
31
|
+
"ServiceClient",
|
|
32
|
+
"ServiceResponse",
|
|
33
|
+
"__version__",
|
|
34
|
+
"check_manifest",
|
|
35
|
+
"load_config",
|
|
36
|
+
"load_manifest",
|
|
37
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|