citta-sdk 7.0.0.8085__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.
- citta_sdk-7.0.0.8085/LICENSE +15 -0
- citta_sdk-7.0.0.8085/MANIFEST.in +8 -0
- citta_sdk-7.0.0.8085/PKG-INFO +346 -0
- citta_sdk-7.0.0.8085/README.md +298 -0
- citta_sdk-7.0.0.8085/citta/__init__.py +13 -0
- citta_sdk-7.0.0.8085/citta/core/__init__.py +32 -0
- citta_sdk-7.0.0.8085/citta/core/auth.py +63 -0
- citta_sdk-7.0.0.8085/citta/core/client.py +195 -0
- citta_sdk-7.0.0.8085/citta/core/errors.py +119 -0
- citta_sdk-7.0.0.8085/citta/core/tls.py +48 -0
- citta_sdk-7.0.0.8085/citta/py.typed +0 -0
- citta_sdk-7.0.0.8085/citta/service/__init__.py +86 -0
- citta_sdk-7.0.0.8085/citta/service/aws.py +132 -0
- citta_sdk-7.0.0.8085/citta/service/contract.py +232 -0
- citta_sdk-7.0.0.8085/citta/service/fastapi.py +128 -0
- citta_sdk-7.0.0.8085/citta/service/files.py +478 -0
- citta_sdk-7.0.0.8085/citta/service/flask.py +101 -0
- citta_sdk-7.0.0.8085/citta/service/pools.py +173 -0
- citta_sdk-7.0.0.8085/citta/service/providers.py +148 -0
- citta_sdk-7.0.0.8085/citta/service/records.py +249 -0
- citta_sdk-7.0.0.8085/citta/service/refusals.py +67 -0
- citta_sdk-7.0.0.8085/citta/service/request.py +76 -0
- citta_sdk-7.0.0.8085/citta/service/service.py +193 -0
- citta_sdk-7.0.0.8085/citta/service/settings.py +88 -0
- citta_sdk-7.0.0.8085/citta_sdk.egg-info/PKG-INFO +346 -0
- citta_sdk-7.0.0.8085/citta_sdk.egg-info/SOURCES.txt +44 -0
- citta_sdk-7.0.0.8085/citta_sdk.egg-info/dependency_links.txt +1 -0
- citta_sdk-7.0.0.8085/citta_sdk.egg-info/requires.txt +24 -0
- citta_sdk-7.0.0.8085/citta_sdk.egg-info/top_level.txt +1 -0
- citta_sdk-7.0.0.8085/pyproject.toml +78 -0
- citta_sdk-7.0.0.8085/setup.cfg +4 -0
- citta_sdk-7.0.0.8085/tests/conftest.py +52 -0
- citta_sdk-7.0.0.8085/tests/test_aws.py +210 -0
- citta_sdk-7.0.0.8085/tests/test_contract.py +100 -0
- citta_sdk-7.0.0.8085/tests/test_core_client.py +119 -0
- citta_sdk-7.0.0.8085/tests/test_core_errors.py +54 -0
- citta_sdk-7.0.0.8085/tests/test_example_conformance.py +136 -0
- citta_sdk-7.0.0.8085/tests/test_fastapi.py +112 -0
- citta_sdk-7.0.0.8085/tests/test_files.py +295 -0
- citta_sdk-7.0.0.8085/tests/test_flask.py +82 -0
- citta_sdk-7.0.0.8085/tests/test_pools.py +116 -0
- citta_sdk-7.0.0.8085/tests/test_providers.py +133 -0
- citta_sdk-7.0.0.8085/tests/test_records.py +200 -0
- citta_sdk-7.0.0.8085/tests/test_refusals.py +37 -0
- citta_sdk-7.0.0.8085/tests/test_request.py +33 -0
- citta_sdk-7.0.0.8085/tests/test_settings.py +39 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Citta SDK License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Citta. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its documentation ("Software") are proprietary to Citta
|
|
6
|
+
and are made available solely for use with the Citta platform by customers
|
|
7
|
+
and partners of Citta under the terms of their agreement with Citta.
|
|
8
|
+
|
|
9
|
+
Except as expressly permitted by that agreement, you may not copy, modify,
|
|
10
|
+
distribute, sublicense, reverse engineer or create derivative works of the
|
|
11
|
+
Software. The Software is provided "AS IS", without warranty of any kind,
|
|
12
|
+
express or implied, and Citta shall not be liable for any claim, damages or
|
|
13
|
+
other liability arising from the use of the Software.
|
|
14
|
+
|
|
15
|
+
Contact devs@citta.ai for licensing questions.
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: citta-sdk
|
|
3
|
+
Version: 7.0.0.8085
|
|
4
|
+
Summary: Python SDK for the Citta platform: build Custom Services that serve under a tenant's domain and read its deployed data model.
|
|
5
|
+
Author-email: Citta <devs@citta.ai>
|
|
6
|
+
Maintainer-email: Citta <devs@citta.ai>
|
|
7
|
+
License-Expression: LicenseRef-Proprietary
|
|
8
|
+
Project-URL: Homepage, https://citta.ai
|
|
9
|
+
Project-URL: Repository, https://bitbucket.org/53352784/citta-python-sdk
|
|
10
|
+
Project-URL: Changelog, https://bitbucket.org/53352784/citta-python-sdk/src/main/README.md#release-notes
|
|
11
|
+
Keywords: citta,custom-service,sdk
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: psycopg[binary,pool]>=3.1
|
|
29
|
+
Provides-Extra: fastapi
|
|
30
|
+
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
31
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == "fastapi"
|
|
32
|
+
Provides-Extra: aws
|
|
33
|
+
Requires-Dist: mangum>=0.17; extra == "aws"
|
|
34
|
+
Provides-Extra: flask
|
|
35
|
+
Requires-Dist: flask>=3; extra == "flask"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
39
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
40
|
+
Requires-Dist: fastapi>=0.110; extra == "dev"
|
|
41
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
42
|
+
Requires-Dist: mangum>=0.17; extra == "dev"
|
|
43
|
+
Requires-Dist: flask>=3; extra == "dev"
|
|
44
|
+
Requires-Dist: ruff; extra == "dev"
|
|
45
|
+
Requires-Dist: build; extra == "dev"
|
|
46
|
+
Requires-Dist: twine; extra == "dev"
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
|
|
49
|
+
# Citta Python SDK (`citta-sdk`)
|
|
50
|
+
|
|
51
|
+
The Python SDK for the Citta platform. Its first layer, `citta.service`, is for **Custom Services**: HTTP backends you
|
|
52
|
+
host (a FastAPI or Flask application, a LangGraph agent) that the platform routes to under a tenant's own domain and
|
|
53
|
+
that read the tenant's deployed data model directly. The platform tells your service, on every request, which user and
|
|
54
|
+
which application instance it is serving and how to reach that instance's data. This SDK turns that contract into a
|
|
55
|
+
few typed objects and a connection pool.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from fastapi import Depends, FastAPI
|
|
59
|
+
from citta.service import ServiceContext
|
|
60
|
+
from citta.service.fastapi import citta_context, citta_service, install
|
|
61
|
+
|
|
62
|
+
app = FastAPI()
|
|
63
|
+
install(app) # reads CITTA_SERVICE_KEY and friends from the environment
|
|
64
|
+
|
|
65
|
+
@app.get("/{session}")
|
|
66
|
+
def hello(request, context: ServiceContext = Depends(citta_context)):
|
|
67
|
+
pool = citta_service(request).pool(context)
|
|
68
|
+
with pool.connection() as conn:
|
|
69
|
+
count = conn.execute("SELECT count(*) FROM my_object").fetchone()[0]
|
|
70
|
+
return f"hello {context.user.user_name} from {context.tenant.domain}: {count} rows"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A complete service is in [`examples/hello_world`](examples/hello_world/README.md).
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install 'citta-sdk[fastapi]' # the SDK plus FastAPI and uvicorn
|
|
79
|
+
pip install 'citta-sdk[flask]' # the SDK plus Flask
|
|
80
|
+
pip install 'citta-sdk[aws]' # adds the Lambda adapter (Mangum)
|
|
81
|
+
pip install citta-sdk # the SDK alone (httpx, psycopg)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Python 3.10 or newer. From a checkout of this repository: `pip install -e '.[fastapi,dev]'`.
|
|
85
|
+
|
|
86
|
+
## How a request reaches your service
|
|
87
|
+
|
|
88
|
+
A Custom Service is registered once per platform environment with a `code`, and each of its **versions** carries a
|
|
89
|
+
label of your choosing (`dev`, `uat`, `prod1.1`), a routing target and a **service key** that is shown exactly once
|
|
90
|
+
when the version is registered or its key rotated. An application pins one service and one version label; every
|
|
91
|
+
tenant that deploys that application resolves the same service.
|
|
92
|
+
|
|
93
|
+
When a user opens the application's service entry, the platform redirects the browser to
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
https://<tenant host>/<accountCode>_<serviceCode>_<versionLabel>/<appInstanceSession>[/<subPath>]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The platform owns that first path segment, strips it and forwards the rest to your version's target. Your service
|
|
100
|
+
receives
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
GET /<appInstanceSession>[/<subPath>]
|
|
104
|
+
Cookie: auth=... the user's own platform session, forwarded unchanged
|
|
105
|
+
X-Forwarded-Host: <tenant host> where the platform is, for this request
|
|
106
|
+
X-Forwarded-Proto: https
|
|
107
|
+
X-Forwarded-Prefix: /<accountCode>_<serviceCode>_<versionLabel>
|
|
108
|
+
X-Forwarded-For: <client address>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The **app-instance session** is the first path segment, taken verbatim. The SDK never parses it and neither should
|
|
112
|
+
your code: it is an opaque string the platform minted for this user and this application instance, and it is passed
|
|
113
|
+
back to the platform exactly as received.
|
|
114
|
+
|
|
115
|
+
## What the SDK does with it
|
|
116
|
+
|
|
117
|
+
On every request, `CittaService.context_for(path, headers)` (or the FastAPI dependency `citta_context`) calls the
|
|
118
|
+
platform's context endpoint on the forwarded host with the user's cookie, the session from the path and your service
|
|
119
|
+
key. When all three agree, the platform releases a `ServiceContext`:
|
|
120
|
+
|
|
121
|
+
| Block | Fields | Meaning |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `user` | `user_id`, `user_name`, `email`, `roles` | The authenticated end user. Re-validated on every request, never cached, so a revoked session fails on its next click. |
|
|
124
|
+
| `tenant` | `tenant_id`, `tenant_name`, `domain` | The tenant whose domain the request came in on. |
|
|
125
|
+
| `app_instance` | `application_instance_id`, `agi_id`, `application_data_type_id`, `agi_schema`, `app_schema`, `app_ext_schema` | The application instance and its three schemas: the application group instance's data, the application's own schema, and the extension schema your service owns. |
|
|
126
|
+
| `org_context` | `org_hierarchy_id` | The organisation hierarchy the user entered under, when the application has one. |
|
|
127
|
+
| `connection` | `host`, `read_host`, `port`, `database`, `user`, `password`, `search_path`, `ssl_mode` | A database login minted for this application instance, valid for your service alone. `host` is the writer, `read_host` the reader. The password never appears in `repr`. |
|
|
128
|
+
| `session_settings` | `settings` (name → value) | The Postgres session settings the platform sets on its own connections for this request: who is acting, which application instance, and the resolved bitemporal properties the object views and merge routines read. `Records` applies them with `SET LOCAL` around every read and write; use `platform_session` for your own SQL. |
|
|
129
|
+
|
|
130
|
+
The user block is handed to your handler and forgotten. The connection block feeds a **pool registry**: the SDK keeps
|
|
131
|
+
one `psycopg` pool per application instance, creates it on first sight, reuses it while the released block names the
|
|
132
|
+
same login, replaces it when the login changes (a redeploy, say) and closes it after five idle minutes. Ask for it with
|
|
133
|
+
`service.pool(context)` (a `psycopg_pool.ConnectionPool`, reader with `read_only=True`) or `await
|
|
134
|
+
service.async_pool(context)` for handlers on an event loop. Every pooled connection has `search_path` set to the three
|
|
135
|
+
schemas, extension schema first, so unqualified table names resolve the way the platform intends.
|
|
136
|
+
|
|
137
|
+
### What the connection can reach
|
|
138
|
+
|
|
139
|
+
The login is a member of the application's data role and owns the extension schema:
|
|
140
|
+
|
|
141
|
+
- **Read** the objects the application holds through its views in `app_schema`: the application's own objects and
|
|
142
|
+
those linked from another application group alike. `Records` reads there; a bitemporal object answers through its
|
|
143
|
+
`current_version_<object>` view, and rows marked deleted are not shown. The tables behind those views, in
|
|
144
|
+
`agi_schema` (or the owning application group's schema, for a linked object), are readable too.
|
|
145
|
+
- **Write through `Records`** (`citta.service.Records(context, connection).write(object, rows)`): a standard bitemporal
|
|
146
|
+
or incremental-snapshot object is merged through its `merge_app_bulk_<object>` routine in `app_schema`, which forwards
|
|
147
|
+
to the owning schema (an existing `external_id` updates, `deleted=True` marks rows deleted); a plain object is
|
|
148
|
+
inserted through its view. Both run inside the platform session
|
|
149
|
+
settings, so `created_by`, `modified_by` and the bitemporal points are what the platform would record for this user.
|
|
150
|
+
Do not bypass the routines with raw DML on a bitemporal object: its views are not updatable and its history would be
|
|
151
|
+
wrong. Reads of a bitemporal object go through its `current_version_<object>` view (`Records.count`, `read_relation`).
|
|
152
|
+
- **Own `app_ext_schema`**: create your own tables there (state, embeddings, caches). Nothing else touches it, and it
|
|
153
|
+
travels with the application instance.
|
|
154
|
+
- Nothing under the platform's own metadata schemas is visible, and no other tenant's data is reachable: the login
|
|
155
|
+
exists per application instance and is released per authenticated request.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from citta.service import Records
|
|
159
|
+
|
|
160
|
+
with citta_service(request).pool(context).connection() as conn:
|
|
161
|
+
records = Records(context, conn)
|
|
162
|
+
records.objects() # ['events', 'greetings']
|
|
163
|
+
records.count("events") # rows of the current version, at the request's as-of
|
|
164
|
+
records.write("greetings", [{"id": 4, "name": "d", "value": 40}])
|
|
165
|
+
records.write("events", [{"external_id": "e-1", "amount": 10, "label": "first"}])
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
Everything is read from the environment by `CittaService.from_env()`, or passed explicitly as `ServiceSettings`.
|
|
171
|
+
|
|
172
|
+
| Variable | Meaning |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `CITTA_SERVICE_KEY` | The version's service key, shown once at registration. Required unless in local mode. |
|
|
175
|
+
| `CITTA_PLATFORM_URL` | The platform origin to call, overriding the request's `X-Forwarded-*` headers. Set it when your service is called directly (a notebook, `curl`) rather than through the platform. |
|
|
176
|
+
| `CITTA_LOCAL_CONTEXT` | Path to a JSON context block. When set, no platform call is made (see local mode below). |
|
|
177
|
+
| `CITTA_CA_BUNDLE` | A CA file to verify the platform's certificate against, for a platform whose certificate a private CA signed. |
|
|
178
|
+
| `CITTA_TLS_INSECURE` | `1` disables certificate verification and logs a warning on every client built. Development only. |
|
|
179
|
+
| `CITTA_POOL_IDLE_SECONDS` | How long an application instance's pool survives without a request. Default 300. |
|
|
180
|
+
| `CITTA_POOL_MIN_SIZE`, `CITTA_POOL_MAX_SIZE` | Pool bounds. Defaults 1 and 4. |
|
|
181
|
+
| `CITTA_PLATFORM_TIMEOUT` | Seconds to wait for the platform. Default 30. |
|
|
182
|
+
|
|
183
|
+
The service fails at startup, naming the setting, when the key is missing outside local mode or when local mode and
|
|
184
|
+
a platform URL are both set.
|
|
185
|
+
|
|
186
|
+
## Errors, and what the browser sees
|
|
187
|
+
|
|
188
|
+
Every failure is a `CittaError` carrying the platform's message verbatim. The FastAPI integration maps them so that
|
|
189
|
+
the user and the operator each learn what concerns them:
|
|
190
|
+
|
|
191
|
+
| Raised | Meaning | HTTP answer |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `CittaSessionError` | The cookie is missing or expired, or the user lost access. | 401 |
|
|
194
|
+
| `CittaServiceConfigError` (with a `kind`) | The platform refused because of the service's or the application's setup: the key authenticates no active version, the key is for another version or service, the application was deployed before it pinned the service, or the application pins no service. | 503 |
|
|
195
|
+
| `RequestShapeError` | The request is not shaped as the platform sends it (no session in the path). | 404 |
|
|
196
|
+
| any other `CittaError`, or an unhandled exception | The platform could not be reached, answered something unexpected, or the handler failed. | 522 |
|
|
197
|
+
|
|
198
|
+
The body is `{"error": "<message>", "status": <code>}` plus `"kind"` on a 503. Register your own exception handler
|
|
199
|
+
for a class to change its mapping.
|
|
200
|
+
|
|
201
|
+
## Three ways to develop
|
|
202
|
+
|
|
203
|
+
**In a notebook or a workspace inside the platform's network.** Run your service there, register a version whose
|
|
204
|
+
label is yours (`dev`), and drive it in two ways: through the browser, by opening the application's service entry on
|
|
205
|
+
that label, or directly with `curl`, supplying a session string and cookie copied from the browser. For the direct
|
|
206
|
+
route set `CITTA_PLATFORM_URL` to the environment's tenant URL, since no forwarded headers arrive.
|
|
207
|
+
|
|
208
|
+
To copy the cookie: open the application in the browser, open the developer tools, Network tab, select any request
|
|
209
|
+
to the platform, and copy the `auth=` value from the request's `Cookie` header and the app-instance session from a
|
|
210
|
+
request path (`/datatypes/<session>/...`). Then
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
curl -H 'Cookie: auth=<value>' http://localhost:8000/<session>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The cookie is the user's session: treat it as a password and let it expire.
|
|
217
|
+
|
|
218
|
+
**Behind the real redirect.** Deploy the version wherever its target points and open the application's service entry.
|
|
219
|
+
Everything the platform sends is real, including a different tenant domain per tenant.
|
|
220
|
+
|
|
221
|
+
**On a laptop without a platform: local mode.** Put a context block in a file (start from
|
|
222
|
+
`examples/hello_world/context.local.example.json`), point its `connection` at a local Postgres holding the
|
|
223
|
+
application's schema bundle, and set `CITTA_LOCAL_CONTEXT` to the file. Every request is served as the user in the
|
|
224
|
+
file; no key, cookie or platform is involved, and the SDK logs one warning at startup so the mode cannot go unnoticed.
|
|
225
|
+
The same service code runs unchanged in all three.
|
|
226
|
+
|
|
227
|
+
The schema bundle's SQL files are numbered in load order (`examples/hello_world/deploy/load-bundle.sh` loads them).
|
|
228
|
+
When the application links objects from another application group, the bundle also creates that group's schema with
|
|
229
|
+
what the application reaches, under its real name, so reads and writes of linked objects behave as on the platform.
|
|
230
|
+
Its tables arrive empty: the owner's rows, its seed rows included, are not in the bundle, so write the rows you need
|
|
231
|
+
through your service.
|
|
232
|
+
|
|
233
|
+
## Running in AWS Lambda
|
|
234
|
+
|
|
235
|
+
In AWS the platform maps the reserved path segment onto the tenant domain as an API Gateway base path mapping, and
|
|
236
|
+
the request reaches your Lambda as a REST proxy event with the segment already stripped. API Gateway does not set
|
|
237
|
+
`X-Forwarded-Host` or `X-Forwarded-Prefix`; `citta.service.aws` restores them from the event before the SDK reads the
|
|
238
|
+
request, so the same service code runs locally and in Lambda:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
# lambda_handler.py
|
|
242
|
+
from citta.service.aws import CittaLambdaHandler
|
|
243
|
+
from app import app # your FastAPI (or any ASGI) application
|
|
244
|
+
|
|
245
|
+
handler = CittaLambdaHandler(app)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Install `citta-sdk[aws,fastapi]` for the Lambda's runtime, set `CITTA_SERVICE_KEY` in the function's environment,
|
|
249
|
+
and point the REST API's `ANY /{proxy+}` and `ANY /` at the function with a Lambda proxy integration. Requests that hit
|
|
250
|
+
the function on its own `execute-api` host did not come through the platform; there the SDK asks for
|
|
251
|
+
`CITTA_PLATFORM_URL`, as it does for any direct call.
|
|
252
|
+
|
|
253
|
+
## Flask
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from flask import Flask
|
|
257
|
+
from citta.service.flask import citta_context, install
|
|
258
|
+
|
|
259
|
+
app = Flask(__name__)
|
|
260
|
+
install(app)
|
|
261
|
+
|
|
262
|
+
@app.get("/<session>")
|
|
263
|
+
@citta_context
|
|
264
|
+
def hello(session, context):
|
|
265
|
+
return f"hello {context.user.user_name}"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Same error mapping as FastAPI; the view receives the context as the `context` keyword argument. Install `citta-sdk[flask]`.
|
|
269
|
+
|
|
270
|
+
## Layout
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
citta/core transport shared by every layer: CittaClient / AsyncCittaClient, the platform URL shape,
|
|
274
|
+
the errors-array convention, TLS settings, token-envelope refresh for scripts
|
|
275
|
+
citta/service Custom Services: ServiceContext, the context providers, pool registries, settings,
|
|
276
|
+
the FastAPI and Flask integrations, the AWS Lambda adapter
|
|
277
|
+
examples/ hello_world
|
|
278
|
+
tests/ pytest, with the platform mocked
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Release notes
|
|
282
|
+
|
|
283
|
+
### 7.0.0
|
|
284
|
+
|
|
285
|
+
First release on PyPI, as `7.0.0.<build>`; the version scheme is described under
|
|
286
|
+
[Releasing to PyPI](#releasing-to-pypi). Carries the 1.1.1 changes:
|
|
287
|
+
|
|
288
|
+
- `Records` reads and writes through the application's schema (`app_schema`) instead of the application group
|
|
289
|
+
instance's schema, so objects linked from another application group are read and written like the application's
|
|
290
|
+
own. Merge-routine objects are written through `merge_app_bulk_<object>`; plain objects are inserted through their
|
|
291
|
+
view.
|
|
292
|
+
- `Records.objects()` and `tables()` list only the objects the application holds, not every object of its application
|
|
293
|
+
group.
|
|
294
|
+
- Rows marked deleted are no longer read: the application's views show only rows that are not deleted.
|
|
295
|
+
- A write to an object with neither a merge routine nor an insertable view raises `NotWritableError`, naming the
|
|
296
|
+
object and the schema.
|
|
297
|
+
- New: `Records.schema` and `Records.merge_routine(table)`.
|
|
298
|
+
- `examples/hello_world/deploy/load-bundle.sh` loads whichever numbered SQL files the schema bundle holds, in order.
|
|
299
|
+
|
|
300
|
+
## Developing the SDK
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
python3.11 -m venv .venv && . .venv/bin/activate
|
|
304
|
+
pip install -e '.[fastapi,aws,flask,dev]'
|
|
305
|
+
pytest
|
|
306
|
+
ruff check citta tests examples
|
|
307
|
+
python -m build
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Releasing to PyPI
|
|
311
|
+
|
|
312
|
+
The distribution is `citta-sdk` on [PyPI](https://pypi.org/project/citta-sdk/); the import package is `citta`.
|
|
313
|
+
|
|
314
|
+
**Versions are `${revision}${changelist}`**, for example `7.0.0.8082`, the scheme the platform's Java builds use.
|
|
315
|
+
Both come from the Bitbucket deployment environment `pypi` (Repository settings > Pipelines > Deployments):
|
|
316
|
+
`revision` holds the prefix with its trailing dot (`7.0.0.`) and `changelist` is `$BITBUCKET_BUILD_NUMBER`, which the
|
|
317
|
+
pipeline substitutes with the run's build number. To start a new line (`7.1.0.`), change `revision` on the
|
|
318
|
+
environment; nothing in the source changes. The checked-in `citta/__init__.py` carries the placeholder `0.0.0.dev0`,
|
|
319
|
+
which `pyproject.toml` reads, so local builds are visibly not releases.
|
|
320
|
+
|
|
321
|
+
Releasing is the custom pipeline `publish-tag-pypi` in `bitbucket-pipelines.yml`: Pipelines > Run pipeline >
|
|
322
|
+
branch `main` > `publish-tag-pypi`. Nothing runs on push. The pipeline
|
|
323
|
+
|
|
324
|
+
1. **Test**: `ruff` and `pytest`.
|
|
325
|
+
2. **Publish to PyPI**: a deployment step to the `pypi` environment. It stamps `${revision}${changelist}` into
|
|
326
|
+
`__version__` with `scripts/stamp-version.py`, runs `python -m build` and `twine check --strict`, and uploads with
|
|
327
|
+
the environment's `PYPI_API_TOKEN` (a token scoped to the `citta-sdk` project). The artifacts are kept on the run.
|
|
328
|
+
|
|
329
|
+
A version that is on PyPI can never be re-uploaded or edited: releasing again means another build from `main`. Add a
|
|
330
|
+
line under [Release notes](#release-notes) for anything users should know, naming the build it shipped in.
|
|
331
|
+
|
|
332
|
+
To rehearse on TestPyPI, run the custom `test-pypi` pipeline (Pipelines > Run pipeline > `main` > `test-pypi`); it
|
|
333
|
+
deploys to the `test-pypi` environment, which carries the same variables with `TEST_PYPI_API_TOKEN`. Then install from
|
|
334
|
+
there into a clean environment:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ 'citta-sdk[fastapi]'
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Local builds carry the placeholder version `0.0.0.dev0` and are for development only; do not upload them.
|
|
341
|
+
|
|
342
|
+
`scripts/make-distribution-zip.sh` builds the zip developers receive (`dist/citta-sdk-<version>.zip`: the wheel and the
|
|
343
|
+
tracked files of `examples/`) from a clean checkout.
|
|
344
|
+
|
|
345
|
+
This package succeeds the earlier `asw_sdk` distribution, which remains available for existing pipeline steps and
|
|
346
|
+
is not extended.
|