cweb-forge 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PyStrap
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.
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: cweb-forge
3
+ Version: 0.1.0
4
+ Summary: Python SDK for cWeb Forge serverless data, for applications and notebooks
5
+ Author: PyStrap
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://cweb.app
8
+ Project-URL: Documentation, https://github.com/pystrap/cweb-forge-python#readme
9
+ Project-URL: Source, https://github.com/pystrap/cweb-forge-python
10
+ Project-URL: Issues, https://github.com/pystrap/cweb-forge-python/issues
11
+ Keywords: cweb,forge,serverless,database,sdk,notebooks
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Database :: Front-Ends
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: httpx<1,>=0.28
20
+ Dynamic: license-file
21
+
22
+ # cWeb Forge · Python
23
+
24
+ **Serverless data and authentication API for your next application.**
25
+
26
+ Use cWeb Forge collections from Python backends, scripts and notebooks without
27
+ building a separate data API. This is the official Python SDK for
28
+ [cWeb Forge](https://cweb.app), with synchronous and asynchronous clients for
29
+ applications on cWeb or any other host.
30
+
31
+ - JSON document operations with collection access rules.
32
+ - Cursor pagination, equality filters and streaming iteration over pages.
33
+ - Version checks for concurrent edits and idempotency keys for create retries.
34
+ - Backend service keys or application-user access tokens.
35
+ - Works with Django, FastAPI, cWeb Studio and other Python notebook environments.
36
+
37
+ [Getting started](#getting-started) · [Async usage](#async-usage) ·
38
+ [Notebooks](#notebooks) · [Contributing](#contributing)
39
+
40
+ ## Installation
41
+
42
+ Requires Python 3.10 or later.
43
+
44
+ The first PyPI release is being prepared. Until it is available, clone this
45
+ repository and install from its local path:
46
+
47
+ ```sh
48
+ python -m pip install ./cweb-forge-python
49
+ ```
50
+
51
+ Once published, install from PyPI:
52
+
53
+ ```sh
54
+ python -m pip install cweb-forge
55
+ ```
56
+
57
+ The package name is `cweb-forge`; import it as `cweb_forge`.
58
+
59
+ ## Getting started
60
+
61
+ 1. Create a Forge application in the [cWeb dashboard](https://portal.cweb.app)
62
+ and copy its API endpoint, including `/v1/`.
63
+ 2. Create a service key restricted to the collections and actions your backend
64
+ needs. Supply it as `CWEB_FORGE_KEY` through your environment or secret manager.
65
+ 3. Use the application's `records` collection to try the SDK:
66
+
67
+ ```python
68
+ import os
69
+ from cweb_forge import ForgeClient
70
+
71
+ endpoint = 'https://my-app.forge.cweb.app/v1/' # Replace with your endpoint.
72
+
73
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
74
+ records = forge.collection('records')
75
+ created = records.create({'title': 'Draft', 'status': 'active'})
76
+ saved = records.update(
77
+ created['id'], {'title': 'Published'}, version=created['version']
78
+ )
79
+ print(saved['id'])
80
+ ```
81
+
82
+ Use a context manager to close connections, or call `forge.close()` when
83
+ finished. A service key may access records across application users according
84
+ to its scopes. Never embed one in frontend code, mobile apps or shared notebooks.
85
+
86
+ ### Application-user tokens
87
+
88
+ `token` can instead be an application user's access token or a callable that
89
+ returns a fresh token before each request. The asynchronous client also accepts
90
+ an async callable. Token validation and collection policies are enforced by the
91
+ service; default owner access keeps a user's documents private to that user.
92
+
93
+ The Python SDK does not perform interactive sign-in or automatically refresh a
94
+ fixed token string. Your application's authentication integration must supply
95
+ and refresh its tokens. Use the
96
+ [JavaScript SDK](https://github.com/pystrap/cweb-forge-js#authentication) for
97
+ browser sign-in. Never use a cWeb dashboard login token as a Forge application token.
98
+
99
+ ### Public reads without a token
100
+
101
+ Set the collection's read policy to **Anyone (public)** under **Collections &
102
+ rules**, then omit `token`:
103
+
104
+ ```python
105
+ from cweb_forge import ForgeClient
106
+
107
+ with ForgeClient('https://my-app.forge.cweb.app/v1/') as forge:
108
+ page = forge.collection('products').list(limit=50)
109
+ for document in page['items']:
110
+ print(document['data'])
111
+ ```
112
+
113
+ Every document and field in that collection is public. Keep private data in
114
+ separate collections. Anonymous writes are not supported, and public reads
115
+ still consume the application's request allowances.
116
+
117
+ ## Working with documents
118
+
119
+ Your JSON object is returned in `document['data']`, alongside metadata including
120
+ `id`, `owner`, `version`, `created_at` and `updated_at`.
121
+
122
+ | Method | Purpose |
123
+ | --- | --- |
124
+ | `list(limit=50, after=None, field=None, equals=None)` | Read a page with `items` and `next_cursor`. |
125
+ | `get(document_id)` | Read one document. |
126
+ | `create(data, idempotency_key=None)` | Create a document; a key is generated if omitted. |
127
+ | `update(document_id, data, version=...)` | Patch the supplied fields. |
128
+ | `replace(document_id, data, version=...)` | Replace the document's data. |
129
+ | `delete(document_id, version=...)` | Delete a document. |
130
+ | `iterate(limit=50, field=None, equals=None)` | Iterate over documents across successive pages. |
131
+
132
+ Collection names start with a lowercase letter, contain lowercase letters,
133
+ numbers or underscores, and have a maximum length of 64 characters. Document
134
+ IDs are UUIDs returned by Forge.
135
+
136
+ ```python
137
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
138
+ records = forge.collection('records')
139
+ page = records.list(limit=50, field='status', equals='active')
140
+ if page['next_cursor']:
141
+ next_page = records.list(
142
+ limit=50, after=page['next_cursor'], field='status', equals='active'
143
+ )
144
+
145
+ for document in records.iterate(limit=100, field='status', equals='active'):
146
+ print(document['id'])
147
+ ```
148
+
149
+ Equality filters accept scalar field values. Page limits range from 1 to 100
150
+ documents. Iteration fetches one page at a time; collecting every result into a
151
+ list still uses memory for the entire result.
152
+
153
+ Use the latest returned `version` for each update, replacement or deletion.
154
+ On a `409` conflict, reload and resolve the change before retrying. To retry a
155
+ create after an uncertain network outcome, reuse the same `idempotency_key`
156
+ for that logical operation within the server's 24-hour window. The SDK does not
157
+ automatically retry writes.
158
+
159
+ ## Async usage
160
+
161
+ Use `AsyncForgeClient` in asynchronous applications. Await document operations,
162
+ and use `async for` when iterating:
163
+
164
+ ```python
165
+ import asyncio
166
+ import os
167
+ from cweb_forge import AsyncForgeClient
168
+
169
+ async def main():
170
+ async with AsyncForgeClient(
171
+ 'https://my-app.forge.cweb.app/v1/',
172
+ token=os.environ['CWEB_FORGE_KEY'],
173
+ ) as forge:
174
+ records = forge.collection('records')
175
+ created = await records.create({'title': 'Async example'})
176
+ print(created['id'])
177
+ async for document in records.iterate(limit=100):
178
+ print(document['id'])
179
+
180
+ asyncio.run(main())
181
+ ```
182
+
183
+ In notebooks with an existing event loop, use `await main()` instead of
184
+ `asyncio.run(main())`. Close a manually managed async client with
185
+ `await forge.close()`.
186
+
187
+ ## Notebooks
188
+
189
+ The SDK can fetch data for pandas, DuckDB or your own analysis code. Those
190
+ libraries are optional and installed separately. Start with a read-only key and
191
+ a bounded sample:
192
+
193
+ ```python
194
+ import os
195
+ import pandas as pd
196
+ from cweb_forge import ForgeClient
197
+
198
+ # Configure this secret in the notebook environment; do not paste it into a cell.
199
+ with ForgeClient(
200
+ 'https://my-app.forge.cweb.app/v1/',
201
+ token=os.environ['CWEB_FORGE_KEY'],
202
+ ) as forge:
203
+ page = forge.collection('sales').list(limit=100)
204
+ frame = pd.DataFrame([document['data'] for document in page['items']])
205
+ ```
206
+
207
+ This reads a page of JSON documents; it does not mount a database or automatically
208
+ copy the whole collection into Studio's catalog. Do not save credentials or
209
+ sensitive results in notebook outputs that you intend to share.
210
+
211
+ ## Errors and timeouts
212
+
213
+ Catch `ForgeError` for API or transport failures. It includes `status`, `code`
214
+ and `request_id`; network failures use status `0`.
215
+
216
+ | Status | Suggested handling |
217
+ | --- | --- |
218
+ | `401` | Renew credentials or sign the application user in again. |
219
+ | `403` | Check collection rules and service-key permissions. |
220
+ | `409` | Resolve a concurrent edit or idempotency conflict. |
221
+ | `429` | Inspect the error code for rate or quota limits before retrying. |
222
+ | `503` | Retry later with bounded backoff when safe. |
223
+
224
+ ```python
225
+ from cweb_forge import ForgeError
226
+
227
+ try:
228
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
229
+ page = forge.collection('records').list()
230
+ except ForgeError as error:
231
+ print(f'Forge request failed: {error.status} {error.code}')
232
+ # Share error.request_id with support if present; never log credentials.
233
+ ```
234
+
235
+ HTTPX timeouts default to 15 seconds and can be configured with `timeout=30`
236
+ on the client. These are transport timeouts, not a deadline for a complete
237
+ multi-page iteration. HTTPS is required except on localhost; redirects and
238
+ proxy settings inherited from environment variables are disabled.
239
+
240
+ ## Security and support
241
+
242
+ Use narrowly scoped keys and rotate them if exposed. Keep keys, application-user
243
+ tokens, private data and authentication headers out of logs and Git commits.
244
+ For vulnerabilities, follow the
245
+ [security policy](https://github.com/pystrap/cweb-forge-python/blob/main/SECURITY.md).
246
+
247
+ Report SDK bugs through
248
+ [GitHub issues](https://github.com/pystrap/cweb-forge-python/issues), with the
249
+ SDK/Python versions and a minimal reproduction. For account or billing help,
250
+ contact cWeb Support at [support@cweb.app](mailto:support@cweb.app).
251
+
252
+ ## Contributing
253
+
254
+ Focused fixes, regression tests and documentation improvements are welcome.
255
+ From a clone of this repository:
256
+
257
+ ```sh
258
+ python -m venv .venv
259
+ . .venv/bin/activate
260
+ python -m pip install -e .
261
+ python -m unittest discover -s tests -v
262
+ ```
263
+
264
+ Tests use mocked HTTP responses and do not require a live Forge account.
265
+ Include tests for changed behavior and never submit customer data or credentials.
266
+
267
+ ## License
268
+
269
+ [MIT](https://github.com/pystrap/cweb-forge-python/blob/main/LICENSE).
270
+ Maintained by [PyStrap](https://pystrap.co.uk), the team behind cWeb.
@@ -0,0 +1,249 @@
1
+ # cWeb Forge · Python
2
+
3
+ **Serverless data and authentication API for your next application.**
4
+
5
+ Use cWeb Forge collections from Python backends, scripts and notebooks without
6
+ building a separate data API. This is the official Python SDK for
7
+ [cWeb Forge](https://cweb.app), with synchronous and asynchronous clients for
8
+ applications on cWeb or any other host.
9
+
10
+ - JSON document operations with collection access rules.
11
+ - Cursor pagination, equality filters and streaming iteration over pages.
12
+ - Version checks for concurrent edits and idempotency keys for create retries.
13
+ - Backend service keys or application-user access tokens.
14
+ - Works with Django, FastAPI, cWeb Studio and other Python notebook environments.
15
+
16
+ [Getting started](#getting-started) · [Async usage](#async-usage) ·
17
+ [Notebooks](#notebooks) · [Contributing](#contributing)
18
+
19
+ ## Installation
20
+
21
+ Requires Python 3.10 or later.
22
+
23
+ The first PyPI release is being prepared. Until it is available, clone this
24
+ repository and install from its local path:
25
+
26
+ ```sh
27
+ python -m pip install ./cweb-forge-python
28
+ ```
29
+
30
+ Once published, install from PyPI:
31
+
32
+ ```sh
33
+ python -m pip install cweb-forge
34
+ ```
35
+
36
+ The package name is `cweb-forge`; import it as `cweb_forge`.
37
+
38
+ ## Getting started
39
+
40
+ 1. Create a Forge application in the [cWeb dashboard](https://portal.cweb.app)
41
+ and copy its API endpoint, including `/v1/`.
42
+ 2. Create a service key restricted to the collections and actions your backend
43
+ needs. Supply it as `CWEB_FORGE_KEY` through your environment or secret manager.
44
+ 3. Use the application's `records` collection to try the SDK:
45
+
46
+ ```python
47
+ import os
48
+ from cweb_forge import ForgeClient
49
+
50
+ endpoint = 'https://my-app.forge.cweb.app/v1/' # Replace with your endpoint.
51
+
52
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
53
+ records = forge.collection('records')
54
+ created = records.create({'title': 'Draft', 'status': 'active'})
55
+ saved = records.update(
56
+ created['id'], {'title': 'Published'}, version=created['version']
57
+ )
58
+ print(saved['id'])
59
+ ```
60
+
61
+ Use a context manager to close connections, or call `forge.close()` when
62
+ finished. A service key may access records across application users according
63
+ to its scopes. Never embed one in frontend code, mobile apps or shared notebooks.
64
+
65
+ ### Application-user tokens
66
+
67
+ `token` can instead be an application user's access token or a callable that
68
+ returns a fresh token before each request. The asynchronous client also accepts
69
+ an async callable. Token validation and collection policies are enforced by the
70
+ service; default owner access keeps a user's documents private to that user.
71
+
72
+ The Python SDK does not perform interactive sign-in or automatically refresh a
73
+ fixed token string. Your application's authentication integration must supply
74
+ and refresh its tokens. Use the
75
+ [JavaScript SDK](https://github.com/pystrap/cweb-forge-js#authentication) for
76
+ browser sign-in. Never use a cWeb dashboard login token as a Forge application token.
77
+
78
+ ### Public reads without a token
79
+
80
+ Set the collection's read policy to **Anyone (public)** under **Collections &
81
+ rules**, then omit `token`:
82
+
83
+ ```python
84
+ from cweb_forge import ForgeClient
85
+
86
+ with ForgeClient('https://my-app.forge.cweb.app/v1/') as forge:
87
+ page = forge.collection('products').list(limit=50)
88
+ for document in page['items']:
89
+ print(document['data'])
90
+ ```
91
+
92
+ Every document and field in that collection is public. Keep private data in
93
+ separate collections. Anonymous writes are not supported, and public reads
94
+ still consume the application's request allowances.
95
+
96
+ ## Working with documents
97
+
98
+ Your JSON object is returned in `document['data']`, alongside metadata including
99
+ `id`, `owner`, `version`, `created_at` and `updated_at`.
100
+
101
+ | Method | Purpose |
102
+ | --- | --- |
103
+ | `list(limit=50, after=None, field=None, equals=None)` | Read a page with `items` and `next_cursor`. |
104
+ | `get(document_id)` | Read one document. |
105
+ | `create(data, idempotency_key=None)` | Create a document; a key is generated if omitted. |
106
+ | `update(document_id, data, version=...)` | Patch the supplied fields. |
107
+ | `replace(document_id, data, version=...)` | Replace the document's data. |
108
+ | `delete(document_id, version=...)` | Delete a document. |
109
+ | `iterate(limit=50, field=None, equals=None)` | Iterate over documents across successive pages. |
110
+
111
+ Collection names start with a lowercase letter, contain lowercase letters,
112
+ numbers or underscores, and have a maximum length of 64 characters. Document
113
+ IDs are UUIDs returned by Forge.
114
+
115
+ ```python
116
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
117
+ records = forge.collection('records')
118
+ page = records.list(limit=50, field='status', equals='active')
119
+ if page['next_cursor']:
120
+ next_page = records.list(
121
+ limit=50, after=page['next_cursor'], field='status', equals='active'
122
+ )
123
+
124
+ for document in records.iterate(limit=100, field='status', equals='active'):
125
+ print(document['id'])
126
+ ```
127
+
128
+ Equality filters accept scalar field values. Page limits range from 1 to 100
129
+ documents. Iteration fetches one page at a time; collecting every result into a
130
+ list still uses memory for the entire result.
131
+
132
+ Use the latest returned `version` for each update, replacement or deletion.
133
+ On a `409` conflict, reload and resolve the change before retrying. To retry a
134
+ create after an uncertain network outcome, reuse the same `idempotency_key`
135
+ for that logical operation within the server's 24-hour window. The SDK does not
136
+ automatically retry writes.
137
+
138
+ ## Async usage
139
+
140
+ Use `AsyncForgeClient` in asynchronous applications. Await document operations,
141
+ and use `async for` when iterating:
142
+
143
+ ```python
144
+ import asyncio
145
+ import os
146
+ from cweb_forge import AsyncForgeClient
147
+
148
+ async def main():
149
+ async with AsyncForgeClient(
150
+ 'https://my-app.forge.cweb.app/v1/',
151
+ token=os.environ['CWEB_FORGE_KEY'],
152
+ ) as forge:
153
+ records = forge.collection('records')
154
+ created = await records.create({'title': 'Async example'})
155
+ print(created['id'])
156
+ async for document in records.iterate(limit=100):
157
+ print(document['id'])
158
+
159
+ asyncio.run(main())
160
+ ```
161
+
162
+ In notebooks with an existing event loop, use `await main()` instead of
163
+ `asyncio.run(main())`. Close a manually managed async client with
164
+ `await forge.close()`.
165
+
166
+ ## Notebooks
167
+
168
+ The SDK can fetch data for pandas, DuckDB or your own analysis code. Those
169
+ libraries are optional and installed separately. Start with a read-only key and
170
+ a bounded sample:
171
+
172
+ ```python
173
+ import os
174
+ import pandas as pd
175
+ from cweb_forge import ForgeClient
176
+
177
+ # Configure this secret in the notebook environment; do not paste it into a cell.
178
+ with ForgeClient(
179
+ 'https://my-app.forge.cweb.app/v1/',
180
+ token=os.environ['CWEB_FORGE_KEY'],
181
+ ) as forge:
182
+ page = forge.collection('sales').list(limit=100)
183
+ frame = pd.DataFrame([document['data'] for document in page['items']])
184
+ ```
185
+
186
+ This reads a page of JSON documents; it does not mount a database or automatically
187
+ copy the whole collection into Studio's catalog. Do not save credentials or
188
+ sensitive results in notebook outputs that you intend to share.
189
+
190
+ ## Errors and timeouts
191
+
192
+ Catch `ForgeError` for API or transport failures. It includes `status`, `code`
193
+ and `request_id`; network failures use status `0`.
194
+
195
+ | Status | Suggested handling |
196
+ | --- | --- |
197
+ | `401` | Renew credentials or sign the application user in again. |
198
+ | `403` | Check collection rules and service-key permissions. |
199
+ | `409` | Resolve a concurrent edit or idempotency conflict. |
200
+ | `429` | Inspect the error code for rate or quota limits before retrying. |
201
+ | `503` | Retry later with bounded backoff when safe. |
202
+
203
+ ```python
204
+ from cweb_forge import ForgeError
205
+
206
+ try:
207
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
208
+ page = forge.collection('records').list()
209
+ except ForgeError as error:
210
+ print(f'Forge request failed: {error.status} {error.code}')
211
+ # Share error.request_id with support if present; never log credentials.
212
+ ```
213
+
214
+ HTTPX timeouts default to 15 seconds and can be configured with `timeout=30`
215
+ on the client. These are transport timeouts, not a deadline for a complete
216
+ multi-page iteration. HTTPS is required except on localhost; redirects and
217
+ proxy settings inherited from environment variables are disabled.
218
+
219
+ ## Security and support
220
+
221
+ Use narrowly scoped keys and rotate them if exposed. Keep keys, application-user
222
+ tokens, private data and authentication headers out of logs and Git commits.
223
+ For vulnerabilities, follow the
224
+ [security policy](https://github.com/pystrap/cweb-forge-python/blob/main/SECURITY.md).
225
+
226
+ Report SDK bugs through
227
+ [GitHub issues](https://github.com/pystrap/cweb-forge-python/issues), with the
228
+ SDK/Python versions and a minimal reproduction. For account or billing help,
229
+ contact cWeb Support at [support@cweb.app](mailto:support@cweb.app).
230
+
231
+ ## Contributing
232
+
233
+ Focused fixes, regression tests and documentation improvements are welcome.
234
+ From a clone of this repository:
235
+
236
+ ```sh
237
+ python -m venv .venv
238
+ . .venv/bin/activate
239
+ python -m pip install -e .
240
+ python -m unittest discover -s tests -v
241
+ ```
242
+
243
+ Tests use mocked HTTP responses and do not require a live Forge account.
244
+ Include tests for changed behavior and never submit customer data or credentials.
245
+
246
+ ## License
247
+
248
+ [MIT](https://github.com/pystrap/cweb-forge-python/blob/main/LICENSE).
249
+ Maintained by [PyStrap](https://pystrap.co.uk), the team behind cWeb.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+ [project]
5
+ name = "cweb-forge"
6
+ version = "0.1.0"
7
+ description = "Python SDK for cWeb Forge serverless data, for applications and notebooks"
8
+ readme = "README.md"
9
+ authors = [{name = "PyStrap"}]
10
+ keywords = ["cweb", "forge", "serverless", "database", "sdk", "notebooks"]
11
+ requires-python = ">=3.10"
12
+ dependencies = ["httpx>=0.28,<1"]
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Topic :: Database :: Front-Ends",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ [project.urls]
22
+ Homepage = "https://cweb.app"
23
+ Documentation = "https://github.com/pystrap/cweb-forge-python#readme"
24
+ Source = "https://github.com/pystrap/cweb-forge-python"
25
+ Issues = "https://github.com/pystrap/cweb-forge-python/issues"
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+ [tool.setuptools.package-data]
29
+ cweb_forge = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,134 @@
1
+ """cWeb Forge: bounded HTTP clients, no hidden global credentials or retries."""
2
+ import asyncio
3
+ import json
4
+ import re
5
+ import uuid
6
+ from urllib.parse import urlsplit
7
+ import httpx
8
+
9
+ __all__ = ['ForgeClient', 'AsyncForgeClient', 'ForgeError']
10
+
11
+
12
+ class ForgeError(Exception):
13
+ def __init__(self, message, status=0, code='network_error', request_id=''):
14
+ super().__init__(message)
15
+ self.status, self.code, self.request_id = status, code, request_id
16
+
17
+
18
+ def _endpoint(url):
19
+ p = urlsplit(url)
20
+ local = p.hostname in ('localhost', '127.0.0.1', '::1') or (p.hostname or '').endswith('.localhost')
21
+ if not p.hostname or (p.scheme != 'https' and not (p.scheme == 'http' and local)) or p.username or p.password or p.query or p.fragment:
22
+ raise ValueError('Use an HTTPS endpoint; HTTP is permitted only for local development.')
23
+ return url.rstrip('/')
24
+
25
+
26
+ def _name(value):
27
+ if not re.fullmatch(r'[a-z][a-z0-9_]{0,63}', value):
28
+ raise ValueError('Invalid collection name.')
29
+ return value
30
+
31
+
32
+ def _result(response):
33
+ if response.status_code == 204:
34
+ return None
35
+ try:
36
+ value = response.json()
37
+ except ValueError:
38
+ raise ForgeError('Forge returned an invalid response.', response.status_code, 'invalid_response')
39
+ if not response.is_success:
40
+ error = value.get('error', {})
41
+ raise ForgeError(error.get('message', 'Forge request failed.'), response.status_code,
42
+ error.get('code', 'request_failed'), response.headers.get('X-Request-ID', ''))
43
+ return value
44
+
45
+
46
+ class ForgeClient:
47
+ def __init__(self, endpoint: str, token=None, *, timeout: float = 15, transport=None):
48
+ self.endpoint, self.token = _endpoint(endpoint), token
49
+ self._http = httpx.Client(timeout=timeout, follow_redirects=False, trust_env=False, transport=transport)
50
+
51
+ def request(self, method, path, *, body=None, headers=None, params=None):
52
+ token = self.token() if callable(self.token) else self.token
53
+ auth = {'Authorization': 'Bearer ' + token} if token else {}
54
+ try:
55
+ return _result(self._http.request(method, self.endpoint + path, json=body, params=params,
56
+ headers={**auth, **(headers or {})}))
57
+ except httpx.HTTPError:
58
+ raise ForgeError('Unable to complete the Forge request.') from None
59
+
60
+ def collection(self, name: str):
61
+ return Collection(self, name)
62
+
63
+ def close(self): self._http.close()
64
+ def __enter__(self): return self
65
+ def __exit__(self, *args): self.close()
66
+
67
+
68
+ class Collection:
69
+ def __init__(self, client, name):
70
+ self.client, self.path = client, '/collections/' + _name(name) + '/documents'
71
+
72
+ def list(self, *, limit=50, after=None, field=None, equals=None):
73
+ params = {'limit': limit}
74
+ if after: params['after'] = after
75
+ if field: params.update(field=field, value=json.dumps(equals, allow_nan=False))
76
+ return self.client.request('GET', self.path, params=params)
77
+
78
+ def get(self, document_id):
79
+ return self.client.request('GET', self.path + '/' + str(uuid.UUID(str(document_id))))
80
+
81
+ def create(self, data, *, idempotency_key=None):
82
+ return self.client.request('POST', self.path, body=data, headers={'Idempotency-Key': idempotency_key or str(uuid.uuid4())})
83
+
84
+ def replace(self, document_id, data, *, version):
85
+ return self.client.request('PUT', self.path + '/' + str(uuid.UUID(str(document_id))), body=data, headers={'If-Match': f'"{version}"'})
86
+
87
+ def update(self, document_id, data, *, version):
88
+ return self.client.request('PATCH', self.path + '/' + str(uuid.UUID(str(document_id))), body=data, headers={'If-Match': f'"{version}"'})
89
+
90
+ def delete(self, document_id, *, version):
91
+ return self.client.request('DELETE', self.path + '/' + str(uuid.UUID(str(document_id))), headers={'If-Match': f'"{version}"'})
92
+
93
+ def iterate(self, **options):
94
+ after = None
95
+ while True:
96
+ result = self.list(after=after, **options)
97
+ yield from result['items']
98
+ cursor = result.get('next_cursor')
99
+ if not cursor: return
100
+ if cursor == after: raise ForgeError('Invalid pagination cursor.', code='invalid_response')
101
+ after = cursor
102
+
103
+
104
+ class AsyncForgeClient:
105
+ def __init__(self, endpoint: str, token=None, *, timeout: float = 15, transport=None):
106
+ self.endpoint, self.token = _endpoint(endpoint), token
107
+ self._http = httpx.AsyncClient(timeout=timeout, follow_redirects=False, trust_env=False, transport=transport)
108
+
109
+ async def request(self, method, path, *, body=None, headers=None, params=None):
110
+ import inspect
111
+ token = self.token() if callable(self.token) else self.token
112
+ if inspect.isawaitable(token): token = await token
113
+ auth = {'Authorization': 'Bearer ' + token} if token else {}
114
+ try:
115
+ return _result(await self._http.request(method, self.endpoint + path, json=body, params=params, headers={**auth, **(headers or {})}))
116
+ except httpx.HTTPError:
117
+ raise ForgeError('Unable to complete the Forge request.') from None
118
+
119
+ def collection(self, name: str): return AsyncCollection(self, name)
120
+ async def close(self): await self._http.aclose()
121
+ async def __aenter__(self): return self
122
+ async def __aexit__(self, *args): await self.close()
123
+
124
+
125
+ class AsyncCollection(Collection):
126
+ async def iterate(self, **options):
127
+ after = None
128
+ while True:
129
+ result = await self.list(after=after, **options)
130
+ for item in result['items']: yield item
131
+ cursor = result.get('next_cursor')
132
+ if not cursor: return
133
+ if cursor == after: raise ForgeError('Invalid pagination cursor.', code='invalid_response')
134
+ after = cursor
File without changes
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: cweb-forge
3
+ Version: 0.1.0
4
+ Summary: Python SDK for cWeb Forge serverless data, for applications and notebooks
5
+ Author: PyStrap
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://cweb.app
8
+ Project-URL: Documentation, https://github.com/pystrap/cweb-forge-python#readme
9
+ Project-URL: Source, https://github.com/pystrap/cweb-forge-python
10
+ Project-URL: Issues, https://github.com/pystrap/cweb-forge-python/issues
11
+ Keywords: cweb,forge,serverless,database,sdk,notebooks
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Database :: Front-Ends
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: httpx<1,>=0.28
20
+ Dynamic: license-file
21
+
22
+ # cWeb Forge · Python
23
+
24
+ **Serverless data and authentication API for your next application.**
25
+
26
+ Use cWeb Forge collections from Python backends, scripts and notebooks without
27
+ building a separate data API. This is the official Python SDK for
28
+ [cWeb Forge](https://cweb.app), with synchronous and asynchronous clients for
29
+ applications on cWeb or any other host.
30
+
31
+ - JSON document operations with collection access rules.
32
+ - Cursor pagination, equality filters and streaming iteration over pages.
33
+ - Version checks for concurrent edits and idempotency keys for create retries.
34
+ - Backend service keys or application-user access tokens.
35
+ - Works with Django, FastAPI, cWeb Studio and other Python notebook environments.
36
+
37
+ [Getting started](#getting-started) · [Async usage](#async-usage) ·
38
+ [Notebooks](#notebooks) · [Contributing](#contributing)
39
+
40
+ ## Installation
41
+
42
+ Requires Python 3.10 or later.
43
+
44
+ The first PyPI release is being prepared. Until it is available, clone this
45
+ repository and install from its local path:
46
+
47
+ ```sh
48
+ python -m pip install ./cweb-forge-python
49
+ ```
50
+
51
+ Once published, install from PyPI:
52
+
53
+ ```sh
54
+ python -m pip install cweb-forge
55
+ ```
56
+
57
+ The package name is `cweb-forge`; import it as `cweb_forge`.
58
+
59
+ ## Getting started
60
+
61
+ 1. Create a Forge application in the [cWeb dashboard](https://portal.cweb.app)
62
+ and copy its API endpoint, including `/v1/`.
63
+ 2. Create a service key restricted to the collections and actions your backend
64
+ needs. Supply it as `CWEB_FORGE_KEY` through your environment or secret manager.
65
+ 3. Use the application's `records` collection to try the SDK:
66
+
67
+ ```python
68
+ import os
69
+ from cweb_forge import ForgeClient
70
+
71
+ endpoint = 'https://my-app.forge.cweb.app/v1/' # Replace with your endpoint.
72
+
73
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
74
+ records = forge.collection('records')
75
+ created = records.create({'title': 'Draft', 'status': 'active'})
76
+ saved = records.update(
77
+ created['id'], {'title': 'Published'}, version=created['version']
78
+ )
79
+ print(saved['id'])
80
+ ```
81
+
82
+ Use a context manager to close connections, or call `forge.close()` when
83
+ finished. A service key may access records across application users according
84
+ to its scopes. Never embed one in frontend code, mobile apps or shared notebooks.
85
+
86
+ ### Application-user tokens
87
+
88
+ `token` can instead be an application user's access token or a callable that
89
+ returns a fresh token before each request. The asynchronous client also accepts
90
+ an async callable. Token validation and collection policies are enforced by the
91
+ service; default owner access keeps a user's documents private to that user.
92
+
93
+ The Python SDK does not perform interactive sign-in or automatically refresh a
94
+ fixed token string. Your application's authentication integration must supply
95
+ and refresh its tokens. Use the
96
+ [JavaScript SDK](https://github.com/pystrap/cweb-forge-js#authentication) for
97
+ browser sign-in. Never use a cWeb dashboard login token as a Forge application token.
98
+
99
+ ### Public reads without a token
100
+
101
+ Set the collection's read policy to **Anyone (public)** under **Collections &
102
+ rules**, then omit `token`:
103
+
104
+ ```python
105
+ from cweb_forge import ForgeClient
106
+
107
+ with ForgeClient('https://my-app.forge.cweb.app/v1/') as forge:
108
+ page = forge.collection('products').list(limit=50)
109
+ for document in page['items']:
110
+ print(document['data'])
111
+ ```
112
+
113
+ Every document and field in that collection is public. Keep private data in
114
+ separate collections. Anonymous writes are not supported, and public reads
115
+ still consume the application's request allowances.
116
+
117
+ ## Working with documents
118
+
119
+ Your JSON object is returned in `document['data']`, alongside metadata including
120
+ `id`, `owner`, `version`, `created_at` and `updated_at`.
121
+
122
+ | Method | Purpose |
123
+ | --- | --- |
124
+ | `list(limit=50, after=None, field=None, equals=None)` | Read a page with `items` and `next_cursor`. |
125
+ | `get(document_id)` | Read one document. |
126
+ | `create(data, idempotency_key=None)` | Create a document; a key is generated if omitted. |
127
+ | `update(document_id, data, version=...)` | Patch the supplied fields. |
128
+ | `replace(document_id, data, version=...)` | Replace the document's data. |
129
+ | `delete(document_id, version=...)` | Delete a document. |
130
+ | `iterate(limit=50, field=None, equals=None)` | Iterate over documents across successive pages. |
131
+
132
+ Collection names start with a lowercase letter, contain lowercase letters,
133
+ numbers or underscores, and have a maximum length of 64 characters. Document
134
+ IDs are UUIDs returned by Forge.
135
+
136
+ ```python
137
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
138
+ records = forge.collection('records')
139
+ page = records.list(limit=50, field='status', equals='active')
140
+ if page['next_cursor']:
141
+ next_page = records.list(
142
+ limit=50, after=page['next_cursor'], field='status', equals='active'
143
+ )
144
+
145
+ for document in records.iterate(limit=100, field='status', equals='active'):
146
+ print(document['id'])
147
+ ```
148
+
149
+ Equality filters accept scalar field values. Page limits range from 1 to 100
150
+ documents. Iteration fetches one page at a time; collecting every result into a
151
+ list still uses memory for the entire result.
152
+
153
+ Use the latest returned `version` for each update, replacement or deletion.
154
+ On a `409` conflict, reload and resolve the change before retrying. To retry a
155
+ create after an uncertain network outcome, reuse the same `idempotency_key`
156
+ for that logical operation within the server's 24-hour window. The SDK does not
157
+ automatically retry writes.
158
+
159
+ ## Async usage
160
+
161
+ Use `AsyncForgeClient` in asynchronous applications. Await document operations,
162
+ and use `async for` when iterating:
163
+
164
+ ```python
165
+ import asyncio
166
+ import os
167
+ from cweb_forge import AsyncForgeClient
168
+
169
+ async def main():
170
+ async with AsyncForgeClient(
171
+ 'https://my-app.forge.cweb.app/v1/',
172
+ token=os.environ['CWEB_FORGE_KEY'],
173
+ ) as forge:
174
+ records = forge.collection('records')
175
+ created = await records.create({'title': 'Async example'})
176
+ print(created['id'])
177
+ async for document in records.iterate(limit=100):
178
+ print(document['id'])
179
+
180
+ asyncio.run(main())
181
+ ```
182
+
183
+ In notebooks with an existing event loop, use `await main()` instead of
184
+ `asyncio.run(main())`. Close a manually managed async client with
185
+ `await forge.close()`.
186
+
187
+ ## Notebooks
188
+
189
+ The SDK can fetch data for pandas, DuckDB or your own analysis code. Those
190
+ libraries are optional and installed separately. Start with a read-only key and
191
+ a bounded sample:
192
+
193
+ ```python
194
+ import os
195
+ import pandas as pd
196
+ from cweb_forge import ForgeClient
197
+
198
+ # Configure this secret in the notebook environment; do not paste it into a cell.
199
+ with ForgeClient(
200
+ 'https://my-app.forge.cweb.app/v1/',
201
+ token=os.environ['CWEB_FORGE_KEY'],
202
+ ) as forge:
203
+ page = forge.collection('sales').list(limit=100)
204
+ frame = pd.DataFrame([document['data'] for document in page['items']])
205
+ ```
206
+
207
+ This reads a page of JSON documents; it does not mount a database or automatically
208
+ copy the whole collection into Studio's catalog. Do not save credentials or
209
+ sensitive results in notebook outputs that you intend to share.
210
+
211
+ ## Errors and timeouts
212
+
213
+ Catch `ForgeError` for API or transport failures. It includes `status`, `code`
214
+ and `request_id`; network failures use status `0`.
215
+
216
+ | Status | Suggested handling |
217
+ | --- | --- |
218
+ | `401` | Renew credentials or sign the application user in again. |
219
+ | `403` | Check collection rules and service-key permissions. |
220
+ | `409` | Resolve a concurrent edit or idempotency conflict. |
221
+ | `429` | Inspect the error code for rate or quota limits before retrying. |
222
+ | `503` | Retry later with bounded backoff when safe. |
223
+
224
+ ```python
225
+ from cweb_forge import ForgeError
226
+
227
+ try:
228
+ with ForgeClient(endpoint, token=os.environ['CWEB_FORGE_KEY']) as forge:
229
+ page = forge.collection('records').list()
230
+ except ForgeError as error:
231
+ print(f'Forge request failed: {error.status} {error.code}')
232
+ # Share error.request_id with support if present; never log credentials.
233
+ ```
234
+
235
+ HTTPX timeouts default to 15 seconds and can be configured with `timeout=30`
236
+ on the client. These are transport timeouts, not a deadline for a complete
237
+ multi-page iteration. HTTPS is required except on localhost; redirects and
238
+ proxy settings inherited from environment variables are disabled.
239
+
240
+ ## Security and support
241
+
242
+ Use narrowly scoped keys and rotate them if exposed. Keep keys, application-user
243
+ tokens, private data and authentication headers out of logs and Git commits.
244
+ For vulnerabilities, follow the
245
+ [security policy](https://github.com/pystrap/cweb-forge-python/blob/main/SECURITY.md).
246
+
247
+ Report SDK bugs through
248
+ [GitHub issues](https://github.com/pystrap/cweb-forge-python/issues), with the
249
+ SDK/Python versions and a minimal reproduction. For account or billing help,
250
+ contact cWeb Support at [support@cweb.app](mailto:support@cweb.app).
251
+
252
+ ## Contributing
253
+
254
+ Focused fixes, regression tests and documentation improvements are welcome.
255
+ From a clone of this repository:
256
+
257
+ ```sh
258
+ python -m venv .venv
259
+ . .venv/bin/activate
260
+ python -m pip install -e .
261
+ python -m unittest discover -s tests -v
262
+ ```
263
+
264
+ Tests use mocked HTTP responses and do not require a live Forge account.
265
+ Include tests for changed behavior and never submit customer data or credentials.
266
+
267
+ ## License
268
+
269
+ [MIT](https://github.com/pystrap/cweb-forge-python/blob/main/LICENSE).
270
+ Maintained by [PyStrap](https://pystrap.co.uk), the team behind cWeb.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/cweb_forge/__init__.py
5
+ src/cweb_forge/py.typed
6
+ src/cweb_forge.egg-info/PKG-INFO
7
+ src/cweb_forge.egg-info/SOURCES.txt
8
+ src/cweb_forge.egg-info/dependency_links.txt
9
+ src/cweb_forge.egg-info/requires.txt
10
+ src/cweb_forge.egg-info/top_level.txt
11
+ tests/test_client.py
@@ -0,0 +1 @@
1
+ httpx<1,>=0.28
@@ -0,0 +1 @@
1
+ cweb_forge
@@ -0,0 +1,29 @@
1
+ import unittest
2
+ import httpx
3
+ from cweb_forge import ForgeClient, AsyncForgeClient, ForgeError
4
+
5
+ URL = 'https://my-app.forge.cweb.app/v1/'
6
+
7
+ class ClientTests(unittest.TestCase):
8
+ def test_headers_and_concurrency(self):
9
+ calls = []
10
+ def handler(request):
11
+ calls.append(request)
12
+ return httpx.Response(200, json={'id': 'example'})
13
+ with ForgeClient(URL, 'fg_secret', transport=httpx.MockTransport(handler)) as client:
14
+ client.collection('records').create({'a': 1}, idempotency_key='stable')
15
+ client.collection('records').update('00000000-0000-4000-8000-000000000001', {'a': 2}, version=1)
16
+ self.assertEqual(calls[0].headers['Idempotency-Key'], 'stable')
17
+ self.assertEqual(calls[1].headers['If-Match'], '"1"')
18
+ self.assertEqual(calls[0].headers['Authorization'], 'Bearer fg_secret')
19
+
20
+ def test_errors_and_redirects(self):
21
+ with ForgeClient(URL, transport=httpx.MockTransport(lambda r: httpx.Response(429, json={'error': {'message': 'Limit', 'code': 'quota_exceeded'}}))) as client:
22
+ with self.assertRaises(ForgeError) as result: client.collection('records').list()
23
+ self.assertEqual(result.exception.code, 'quota_exceeded')
24
+ with self.assertRaises(ValueError): ForgeClient('http://example.org')
25
+
26
+ class AsyncTests(unittest.IsolatedAsyncioTestCase):
27
+ async def test_async_iteration(self):
28
+ async with AsyncForgeClient(URL, transport=httpx.MockTransport(lambda r: httpx.Response(200, json={'items': [{'id': '1'}], 'next_cursor': None}))) as client:
29
+ self.assertEqual([x async for x in client.collection('records').iterate()], [{'id': '1'}])