lowcoai-docs 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,17 @@
1
+ node_modules
2
+ npm/engage/dist
3
+ dist
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *.egg-info/
9
+ .venv/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ build/
14
+
15
+ # Dart / Flutter
16
+ .dart_tool/
17
+ pubspec.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lowco AI
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,271 @@
1
+ Metadata-Version: 2.5
2
+ Name: lowcoai-docs
3
+ Version: 0.1.0
4
+ Summary: Official Python client for the lowco document service (files, folders, sharing, triggers, webhooks).
5
+ Project-URL: Repository, https://github.com/lowcoai/lowco-sdk
6
+ Project-URL: Source, https://github.com/lowcoai/lowco-sdk/tree/main/python/docs
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: client,documents,files,lowco,sdk,sharing,storage
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: httpx<1,>=0.25
15
+ Requires-Dist: typing-extensions>=4.7
16
+ Provides-Extra: dev
17
+ Requires-Dist: mypy>=1.10; extra == 'dev'
18
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
19
+ Requires-Dist: pytest>=8; extra == 'dev'
20
+ Requires-Dist: ruff>=0.5; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # lowcoai-docs
24
+
25
+ Python client for the lowco **document service**: files, folders, sharing, triggers and webhooks (routes under `/v1/documents/*`). Sync and asyncio clients with the same surface, built on [httpx](https://www.python-httpx.org/). Python 3.10+.
26
+
27
+ ```bash
28
+ pip install lowcoai-docs
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```python
34
+ from lowcoai.docs import DocsClient
35
+
36
+ with DocsClient("<token-or-api-key>", org_id="org_123") as client:
37
+ bucket = client.buckets.get()["name"] # the org's bucket, created on first use
38
+
39
+ client.folders.create(bucket, {"parentName": "", "folderName": "projects/2026"})
40
+ doc = client.files.upload(bucket, ("plan.md", b"# Plan"), parent_id="projects/2026")
41
+
42
+ for item in client.folders.list(bucket, prefix="projects/2026"):
43
+ print(item["type"], item["path"], item.get("size"))
44
+ ```
45
+
46
+ ### asyncio
47
+
48
+ ```python
49
+ from lowcoai.docs import AsyncDocsClient
50
+
51
+ async with AsyncDocsClient("<token-or-api-key>", org_id="org_123") as client:
52
+ bucket = (await client.buckets.get())["name"]
53
+ hits = await client.search(bucket, "quarterly report")
54
+ ```
55
+
56
+ `AsyncDocsClient` has the same resources and methods as `DocsClient`; each endpoint method is a coroutine. Close it with `await client.aclose()` if you don't use `async with`.
57
+
58
+ ## Options
59
+
60
+ | Argument | Description |
61
+ | ------------- | ------------------------------------------------------------------------------------ |
62
+ | `token` | User token or API key, sent as `Authorization: Bearer <token>`. Required. |
63
+ | `org_id` | Organization id, sent as the `X-Org-Id` header. Required — the service rejects every request without it. |
64
+ | `timeout` | Per-request timeout in seconds (default `30`). `None` disables it. |
65
+ | `headers` | Extra headers added to every request. An `Authorization` or `X-Org-Id` entry here wins over `token` / `org_id`. |
66
+ | `http_client` | Your own `httpx.Client` / `httpx.AsyncClient` (proxies, retries, test transports). It is not closed by the SDK. |
67
+
68
+ All requests go to `https://api.lowco.ai`. An empty `token` or `org_id` raises `ValueError`. The caller's identity comes from the token; the SDK sends no `X-User-Id` (add one through `headers` if your setup needs it).
69
+
70
+ ## Data shapes
71
+
72
+ Request and response bodies are plain dicts typed as `TypedDict`s generated from the service's OpenAPI spec (`Document`, `FileContent`, `NodeView`, `Share`, `ShareLink`, `FolderConfig`, `Trigger`, `Webhook`, …), with keys exactly as on the wire (camelCase). A value returned by one call can be sent back as the body of another. The service's `{"status": 1, "data": ...}` envelope is unwrapped for you; calls whose payload is a confirmation message (most deletes, `star`, `unstar`, …) return that string.
73
+
74
+ `bucket` is the bucket name (`client.buckets.get()["name"]`). Paths are bucket keys such as `projects/plan.md` or `.users/u-123/notes/todo.md`. In URLs, single-segment parameters (bucket, ids, keys, tokens, app keys) are fully percent-encoded, while file and folder paths keep their `/` and have each segment encoded (a leading `/` is dropped). Segments that are exactly `.` or `..` are rejected with `DocsError` before anything is sent. Optional query arguments left as `None` are omitted.
75
+
76
+ ## Surface
77
+
78
+ | Resource | Methods |
79
+ | ------------------- | ------- |
80
+ | `client` | `health()` (plain text), `search(bucket, q)` |
81
+ | `client.buckets` | `get()`, `stats(bucket)` |
82
+ | `client.folders` | `list(bucket, prefix=, sizes=)`, `list_at(bucket, path, sizes=)`, `list_public(bucket, prefix=, sizes=)`, `list_user(bucket, user_id, prefix=, sizes=)`, `list_app(bucket, app_id, prefix=, sizes=)`, `get(bucket, key)`, `create(bucket, body)`, `delete(bucket, key)`, `delete_by_path(bucket, path)`, `upload(bucket, files, relative_paths=, parent_id=, prefix=)`, `download_zip(bucket, path)`, `duplicate(bucket, body)`, `rename(bucket, body)` |
83
+ | `client.files` | `get(bucket, path)`, `read(bucket, path, meta=)`, `create_blank(bucket, body)`, `update(bucket, body)`, `update_at(bucket, path, body)`, `rename(bucket, body)`, `delete(bucket, path)`, `delete_by_path(bucket, path)`, `upload(bucket, file, parent_id=, on_conflict=)`, `download_url(bucket, path)`, `preview_url(bucket, path)`, `list_archive(bucket, path)` |
84
+ | `client.nodes` | `get(node_id)`, `resolve(node_id, download=False)`, `content(node_id)` |
85
+ | `client.library` | `star(bucket, node_id)`, `unstar(bucket, node_id)`, `star_path(bucket, path, type=)`, `unstar_path(bucket, path)`, `starred(bucket)`, `recent(bucket, limit=)`, `trash(bucket)`, `restore(bucket, node_id)`, `purge(bucket, node_id)` |
86
+ | `client.sharing` | `create(bucket, body)`, `list(bucket, path, type=)`, `delete(bucket, id)`, `shared_with_me(bucket, app_key=)`, `create_link(bucket, body)`, `list_links(bucket, path, type=)`, `delete_link(bucket, id)`, `resolve_link(token)` |
87
+ | `client.automation` | `list(bucket, prefix=)`, `create(bucket, body)`, `get(bucket, id)`, `update(bucket, id, body)`, `delete(bucket, id)`, `jobs(bucket, config_id=, limit=)` |
88
+ | `client.app_files` | `upload(app_key, file, path=, on_conflict=)`, `list(app_key, prefix=)`, `delete(app_key, path)`, `sweep(app_key, body)` |
89
+ | `client.triggers` | `list()`, `create(body)`, `get(id)`, `update(id, body)`, `delete(id)` |
90
+ | `client.webhooks` | `list(page=, limit=)`, `create(body)`, `get(id)`, `update(id, body)`, `delete(id)` |
91
+
92
+ `triggers.delete` and `webhooks.delete` return `None`.
93
+
94
+ ## Examples
95
+
96
+ ### Folders
97
+
98
+ ```python
99
+ # sizes=False skips recursive folder sizes (folders then report size 0)
100
+ client.folders.list(bucket, prefix="projects", sizes=False)
101
+ client.folders.list_user(bucket, "u-123", prefix="notes") # a personal drive
102
+ client.folders.rename(
103
+ bucket, {"parentName": "projects", "oldName": "2025", "newName": "archive/2025"}
104
+ )
105
+ client.folders.delete_by_path(bucket, "projects/old/") # any depth; goes to the trash
106
+ ```
107
+
108
+ ### Files
109
+
110
+ ```python
111
+ current = client.files.read(bucket, "notes/todo.md") # content + etag + role
112
+ client.files.update(
113
+ bucket,
114
+ {
115
+ "parentName": "notes",
116
+ "fileName": "todo.md",
117
+ "content": "# Todo\n- ship",
118
+ "ifMatch": current["etag"],
119
+ },
120
+ ) # DocsError(status=412) when someone saved in between
121
+ client.files.create_blank(bucket, {"parentName": "notes", "fileName": "ideas.md"})
122
+ url = client.files.download_url(bucket, "reports/q3.pdf") # short-lived signed URL
123
+ ```
124
+
125
+ ### Nodes (permalinks)
126
+
127
+ ```python
128
+ result = client.nodes.resolve("7489521000000000042", download=True)
129
+ if result.url: # 307: a fresh short-lived URL
130
+ print(result.url)
131
+ elif result.content: # 200: bytes of a file just restored from cold storage
132
+ open(result.content.file_name or "file", "wb").write(result.content.data)
133
+ elif result.restoring: # 202: restore in progress
134
+ print(f"retry in {result.retry_after}s:", result.restoring["message"])
135
+ ```
136
+
137
+ `nodes.content(node_id)` streams the bytes instead of redirecting (`result.content`, or `result.restoring` while a restore runs).
138
+
139
+ ### Library
140
+
141
+ ```python
142
+ client.library.star_path(bucket, "projects/plan.md")
143
+ client.library.recent(bucket, limit=20)
144
+ trashed = client.library.trash(bucket)
145
+ client.library.restore(bucket, trashed[0]["id"])
146
+ ```
147
+
148
+ ### Sharing
149
+
150
+ ```python
151
+ client.sharing.create(
152
+ bucket,
153
+ {
154
+ "path": ".users/u-123/q3.pdf",
155
+ "type": "file",
156
+ "subjectType": "user",
157
+ "subjectId": "u-456",
158
+ "role": "viewer",
159
+ },
160
+ )
161
+ grants = client.sharing.list(bucket, ".users/u-123/q3.pdf")
162
+ link = client.sharing.create_link(bucket, {"path": ".users/u-123/q3.pdf", "type": "file"})
163
+ file_url = client.sharing.resolve_link(link["token"])
164
+ ```
165
+
166
+ ### Automation
167
+
168
+ ```python
169
+ rule = client.automation.create(
170
+ bucket,
171
+ {
172
+ "prefix": "invoices/",
173
+ "pipelineType": "custom_workflow",
174
+ "workflowId": "wf_1",
175
+ "fileSuffixes": [".pdf"],
176
+ },
177
+ )
178
+ client.automation.jobs(bucket, config_id=rule["id"], limit=10)
179
+ ```
180
+
181
+ ### App files
182
+
183
+ ```python
184
+ stored = client.app_files.upload("invoicing", ("INV-0042.pdf", pdf_bytes), path="2026/invoices")
185
+ stored["permalink"] # store this (or stored["nodeId"]), not a storage URL
186
+ client.app_files.sweep("invoicing", {"prefix": "2026", "olderThanDays": 365})
187
+ ```
188
+
189
+ ### Triggers and webhooks
190
+
191
+ ```python
192
+ client.triggers.create(
193
+ {
194
+ "eventType": "create",
195
+ "prefix": "invoices/",
196
+ "suffix": ".pdf",
197
+ "workflowId": "wf_1",
198
+ "active": True,
199
+ }
200
+ )
201
+ client.webhooks.create(
202
+ {
203
+ "url": "https://hooks.example.com/docs",
204
+ "method": "POST",
205
+ "prefix": "uploads/",
206
+ "eventTypes": ["create", "delete"],
207
+ "active": True,
208
+ }
209
+ )
210
+ client.webhooks.list(page=1, limit=50)
211
+ ```
212
+
213
+ ## Uploads
214
+
215
+ Upload methods take a file as an `UploadFile(name, data, content_type=None)` or a `(name, data)` / `(name, data, content_type)` tuple, where `data` is `bytes` or a binary file object (streamed). Without a content type, one is guessed from the name (else `application/octet-stream`). Form fields use the service's exact names:
216
+
217
+ | Method | Parts |
218
+ | ------------------- | ----- |
219
+ | `files.upload` | `file`, `ParentID` (from `parent_id`), `onConflict` |
220
+ | `folders.upload` | repeated `files`, repeated `relativePaths` (same order; one per file), `parentID`, `prefix` |
221
+ | `app_files.upload` | `file`, `path`, `onConflict` |
222
+
223
+ ```python
224
+ from lowcoai.docs import UploadFile
225
+
226
+ with open("photo.jpg", "rb") as fh:
227
+ client.folders.upload(
228
+ bucket,
229
+ [UploadFile("photo.jpg", fh), ("notes.txt", b"hello")],
230
+ relative_paths=["trip/photo.jpg", "trip/notes.txt"],
231
+ parent_id="projects",
232
+ )
233
+ ```
234
+
235
+ ## Redirects and downloads
236
+
237
+ - `files.download_url`, `files.preview_url` and `sharing.resolve_link` answer with a 307 redirect. The SDK never follows it (even if your `httpx` client is configured to) and returns the `Location` URL as a string.
238
+ - `folders.download_zip` returns a `Binary(data, content_type, file_name)`; `file_name` comes from the `X-File-Name` header, else from `Content-Disposition`.
239
+ - `nodes.resolve` / `nodes.content` return a `NodeFileResult` with exactly one of `url`, `content` (`Binary`) or `restoring` (`ArchiveRestoring`) set, plus `retry_after` (seconds, from `Retry-After`) on a 202.
240
+ - `health()` returns the plain-text body (`"Working!"`); it is served at `/health`, outside `/v1/documents`.
241
+
242
+ ### Endpoints the SDK does not wrap
243
+
244
+ `HttpClient` / `AsyncHttpClient` are the transports behind the resources and apply the same headers, envelope unwrapping and error mapping (`request`, `request_void`, `request_text`, `request_binary`, `request_redirect`, `request_node_file`, `request_multipart`):
245
+
246
+ ```python
247
+ from lowcoai.docs import HttpClient
248
+
249
+ with HttpClient("<token>", org_id="org_123") as http:
250
+ data = http.request("GET", "/v1/documents/my-bucket/objects", query={"prefix": "a"})
251
+ ```
252
+
253
+ ## Errors
254
+
255
+ Every non-2xx response raises `DocsError`:
256
+
257
+ ```python
258
+ from lowcoai.docs import DocsError
259
+
260
+ try:
261
+ client.files.read(bucket, "missing.md")
262
+ except DocsError as err:
263
+ print(err.status, err.message, err.code, err.payload)
264
+ ```
265
+
266
+ - `message` — the best message in the error body. The service sends either `{"status": 0, "error": {"message", "code", "details"}}` or `{"message": "..."}`. When `error.message` is a platform code such as `AAS-00106`, `details` is the message and the code goes to `code`; otherwise `error.message` (or `message`) is used, falling back to `"Request failed with status <n>"`.
267
+ - `status` — the HTTP status; `0` when no response arrived (connection error, timeout — the `httpx` exception is chained as `__cause__`) or a `.` / `..` path segment was refused.
268
+ - `code` — the platform error code (e.g. `AAS-00106`) when the body carries one, else `None`.
269
+ - `payload` — the parsed JSON error body (a failed conditional save puts the file's current `currentEtag` / `updatedBy` under `payload["data"]`); the raw text when the body is not JSON; `None` when empty.
270
+
271
+ A 2xx response whose body is not JSON raises `DocsError` with that 2xx `status` and the raw text as `payload` (`triggers.delete` / `webhooks.delete` ignore the body). A redirect method that gets no 3xx `Location` raises `DocsError` too.
@@ -0,0 +1,249 @@
1
+ # lowcoai-docs
2
+
3
+ Python client for the lowco **document service**: files, folders, sharing, triggers and webhooks (routes under `/v1/documents/*`). Sync and asyncio clients with the same surface, built on [httpx](https://www.python-httpx.org/). Python 3.10+.
4
+
5
+ ```bash
6
+ pip install lowcoai-docs
7
+ ```
8
+
9
+ ## Quick start
10
+
11
+ ```python
12
+ from lowcoai.docs import DocsClient
13
+
14
+ with DocsClient("<token-or-api-key>", org_id="org_123") as client:
15
+ bucket = client.buckets.get()["name"] # the org's bucket, created on first use
16
+
17
+ client.folders.create(bucket, {"parentName": "", "folderName": "projects/2026"})
18
+ doc = client.files.upload(bucket, ("plan.md", b"# Plan"), parent_id="projects/2026")
19
+
20
+ for item in client.folders.list(bucket, prefix="projects/2026"):
21
+ print(item["type"], item["path"], item.get("size"))
22
+ ```
23
+
24
+ ### asyncio
25
+
26
+ ```python
27
+ from lowcoai.docs import AsyncDocsClient
28
+
29
+ async with AsyncDocsClient("<token-or-api-key>", org_id="org_123") as client:
30
+ bucket = (await client.buckets.get())["name"]
31
+ hits = await client.search(bucket, "quarterly report")
32
+ ```
33
+
34
+ `AsyncDocsClient` has the same resources and methods as `DocsClient`; each endpoint method is a coroutine. Close it with `await client.aclose()` if you don't use `async with`.
35
+
36
+ ## Options
37
+
38
+ | Argument | Description |
39
+ | ------------- | ------------------------------------------------------------------------------------ |
40
+ | `token` | User token or API key, sent as `Authorization: Bearer <token>`. Required. |
41
+ | `org_id` | Organization id, sent as the `X-Org-Id` header. Required — the service rejects every request without it. |
42
+ | `timeout` | Per-request timeout in seconds (default `30`). `None` disables it. |
43
+ | `headers` | Extra headers added to every request. An `Authorization` or `X-Org-Id` entry here wins over `token` / `org_id`. |
44
+ | `http_client` | Your own `httpx.Client` / `httpx.AsyncClient` (proxies, retries, test transports). It is not closed by the SDK. |
45
+
46
+ All requests go to `https://api.lowco.ai`. An empty `token` or `org_id` raises `ValueError`. The caller's identity comes from the token; the SDK sends no `X-User-Id` (add one through `headers` if your setup needs it).
47
+
48
+ ## Data shapes
49
+
50
+ Request and response bodies are plain dicts typed as `TypedDict`s generated from the service's OpenAPI spec (`Document`, `FileContent`, `NodeView`, `Share`, `ShareLink`, `FolderConfig`, `Trigger`, `Webhook`, …), with keys exactly as on the wire (camelCase). A value returned by one call can be sent back as the body of another. The service's `{"status": 1, "data": ...}` envelope is unwrapped for you; calls whose payload is a confirmation message (most deletes, `star`, `unstar`, …) return that string.
51
+
52
+ `bucket` is the bucket name (`client.buckets.get()["name"]`). Paths are bucket keys such as `projects/plan.md` or `.users/u-123/notes/todo.md`. In URLs, single-segment parameters (bucket, ids, keys, tokens, app keys) are fully percent-encoded, while file and folder paths keep their `/` and have each segment encoded (a leading `/` is dropped). Segments that are exactly `.` or `..` are rejected with `DocsError` before anything is sent. Optional query arguments left as `None` are omitted.
53
+
54
+ ## Surface
55
+
56
+ | Resource | Methods |
57
+ | ------------------- | ------- |
58
+ | `client` | `health()` (plain text), `search(bucket, q)` |
59
+ | `client.buckets` | `get()`, `stats(bucket)` |
60
+ | `client.folders` | `list(bucket, prefix=, sizes=)`, `list_at(bucket, path, sizes=)`, `list_public(bucket, prefix=, sizes=)`, `list_user(bucket, user_id, prefix=, sizes=)`, `list_app(bucket, app_id, prefix=, sizes=)`, `get(bucket, key)`, `create(bucket, body)`, `delete(bucket, key)`, `delete_by_path(bucket, path)`, `upload(bucket, files, relative_paths=, parent_id=, prefix=)`, `download_zip(bucket, path)`, `duplicate(bucket, body)`, `rename(bucket, body)` |
61
+ | `client.files` | `get(bucket, path)`, `read(bucket, path, meta=)`, `create_blank(bucket, body)`, `update(bucket, body)`, `update_at(bucket, path, body)`, `rename(bucket, body)`, `delete(bucket, path)`, `delete_by_path(bucket, path)`, `upload(bucket, file, parent_id=, on_conflict=)`, `download_url(bucket, path)`, `preview_url(bucket, path)`, `list_archive(bucket, path)` |
62
+ | `client.nodes` | `get(node_id)`, `resolve(node_id, download=False)`, `content(node_id)` |
63
+ | `client.library` | `star(bucket, node_id)`, `unstar(bucket, node_id)`, `star_path(bucket, path, type=)`, `unstar_path(bucket, path)`, `starred(bucket)`, `recent(bucket, limit=)`, `trash(bucket)`, `restore(bucket, node_id)`, `purge(bucket, node_id)` |
64
+ | `client.sharing` | `create(bucket, body)`, `list(bucket, path, type=)`, `delete(bucket, id)`, `shared_with_me(bucket, app_key=)`, `create_link(bucket, body)`, `list_links(bucket, path, type=)`, `delete_link(bucket, id)`, `resolve_link(token)` |
65
+ | `client.automation` | `list(bucket, prefix=)`, `create(bucket, body)`, `get(bucket, id)`, `update(bucket, id, body)`, `delete(bucket, id)`, `jobs(bucket, config_id=, limit=)` |
66
+ | `client.app_files` | `upload(app_key, file, path=, on_conflict=)`, `list(app_key, prefix=)`, `delete(app_key, path)`, `sweep(app_key, body)` |
67
+ | `client.triggers` | `list()`, `create(body)`, `get(id)`, `update(id, body)`, `delete(id)` |
68
+ | `client.webhooks` | `list(page=, limit=)`, `create(body)`, `get(id)`, `update(id, body)`, `delete(id)` |
69
+
70
+ `triggers.delete` and `webhooks.delete` return `None`.
71
+
72
+ ## Examples
73
+
74
+ ### Folders
75
+
76
+ ```python
77
+ # sizes=False skips recursive folder sizes (folders then report size 0)
78
+ client.folders.list(bucket, prefix="projects", sizes=False)
79
+ client.folders.list_user(bucket, "u-123", prefix="notes") # a personal drive
80
+ client.folders.rename(
81
+ bucket, {"parentName": "projects", "oldName": "2025", "newName": "archive/2025"}
82
+ )
83
+ client.folders.delete_by_path(bucket, "projects/old/") # any depth; goes to the trash
84
+ ```
85
+
86
+ ### Files
87
+
88
+ ```python
89
+ current = client.files.read(bucket, "notes/todo.md") # content + etag + role
90
+ client.files.update(
91
+ bucket,
92
+ {
93
+ "parentName": "notes",
94
+ "fileName": "todo.md",
95
+ "content": "# Todo\n- ship",
96
+ "ifMatch": current["etag"],
97
+ },
98
+ ) # DocsError(status=412) when someone saved in between
99
+ client.files.create_blank(bucket, {"parentName": "notes", "fileName": "ideas.md"})
100
+ url = client.files.download_url(bucket, "reports/q3.pdf") # short-lived signed URL
101
+ ```
102
+
103
+ ### Nodes (permalinks)
104
+
105
+ ```python
106
+ result = client.nodes.resolve("7489521000000000042", download=True)
107
+ if result.url: # 307: a fresh short-lived URL
108
+ print(result.url)
109
+ elif result.content: # 200: bytes of a file just restored from cold storage
110
+ open(result.content.file_name or "file", "wb").write(result.content.data)
111
+ elif result.restoring: # 202: restore in progress
112
+ print(f"retry in {result.retry_after}s:", result.restoring["message"])
113
+ ```
114
+
115
+ `nodes.content(node_id)` streams the bytes instead of redirecting (`result.content`, or `result.restoring` while a restore runs).
116
+
117
+ ### Library
118
+
119
+ ```python
120
+ client.library.star_path(bucket, "projects/plan.md")
121
+ client.library.recent(bucket, limit=20)
122
+ trashed = client.library.trash(bucket)
123
+ client.library.restore(bucket, trashed[0]["id"])
124
+ ```
125
+
126
+ ### Sharing
127
+
128
+ ```python
129
+ client.sharing.create(
130
+ bucket,
131
+ {
132
+ "path": ".users/u-123/q3.pdf",
133
+ "type": "file",
134
+ "subjectType": "user",
135
+ "subjectId": "u-456",
136
+ "role": "viewer",
137
+ },
138
+ )
139
+ grants = client.sharing.list(bucket, ".users/u-123/q3.pdf")
140
+ link = client.sharing.create_link(bucket, {"path": ".users/u-123/q3.pdf", "type": "file"})
141
+ file_url = client.sharing.resolve_link(link["token"])
142
+ ```
143
+
144
+ ### Automation
145
+
146
+ ```python
147
+ rule = client.automation.create(
148
+ bucket,
149
+ {
150
+ "prefix": "invoices/",
151
+ "pipelineType": "custom_workflow",
152
+ "workflowId": "wf_1",
153
+ "fileSuffixes": [".pdf"],
154
+ },
155
+ )
156
+ client.automation.jobs(bucket, config_id=rule["id"], limit=10)
157
+ ```
158
+
159
+ ### App files
160
+
161
+ ```python
162
+ stored = client.app_files.upload("invoicing", ("INV-0042.pdf", pdf_bytes), path="2026/invoices")
163
+ stored["permalink"] # store this (or stored["nodeId"]), not a storage URL
164
+ client.app_files.sweep("invoicing", {"prefix": "2026", "olderThanDays": 365})
165
+ ```
166
+
167
+ ### Triggers and webhooks
168
+
169
+ ```python
170
+ client.triggers.create(
171
+ {
172
+ "eventType": "create",
173
+ "prefix": "invoices/",
174
+ "suffix": ".pdf",
175
+ "workflowId": "wf_1",
176
+ "active": True,
177
+ }
178
+ )
179
+ client.webhooks.create(
180
+ {
181
+ "url": "https://hooks.example.com/docs",
182
+ "method": "POST",
183
+ "prefix": "uploads/",
184
+ "eventTypes": ["create", "delete"],
185
+ "active": True,
186
+ }
187
+ )
188
+ client.webhooks.list(page=1, limit=50)
189
+ ```
190
+
191
+ ## Uploads
192
+
193
+ Upload methods take a file as an `UploadFile(name, data, content_type=None)` or a `(name, data)` / `(name, data, content_type)` tuple, where `data` is `bytes` or a binary file object (streamed). Without a content type, one is guessed from the name (else `application/octet-stream`). Form fields use the service's exact names:
194
+
195
+ | Method | Parts |
196
+ | ------------------- | ----- |
197
+ | `files.upload` | `file`, `ParentID` (from `parent_id`), `onConflict` |
198
+ | `folders.upload` | repeated `files`, repeated `relativePaths` (same order; one per file), `parentID`, `prefix` |
199
+ | `app_files.upload` | `file`, `path`, `onConflict` |
200
+
201
+ ```python
202
+ from lowcoai.docs import UploadFile
203
+
204
+ with open("photo.jpg", "rb") as fh:
205
+ client.folders.upload(
206
+ bucket,
207
+ [UploadFile("photo.jpg", fh), ("notes.txt", b"hello")],
208
+ relative_paths=["trip/photo.jpg", "trip/notes.txt"],
209
+ parent_id="projects",
210
+ )
211
+ ```
212
+
213
+ ## Redirects and downloads
214
+
215
+ - `files.download_url`, `files.preview_url` and `sharing.resolve_link` answer with a 307 redirect. The SDK never follows it (even if your `httpx` client is configured to) and returns the `Location` URL as a string.
216
+ - `folders.download_zip` returns a `Binary(data, content_type, file_name)`; `file_name` comes from the `X-File-Name` header, else from `Content-Disposition`.
217
+ - `nodes.resolve` / `nodes.content` return a `NodeFileResult` with exactly one of `url`, `content` (`Binary`) or `restoring` (`ArchiveRestoring`) set, plus `retry_after` (seconds, from `Retry-After`) on a 202.
218
+ - `health()` returns the plain-text body (`"Working!"`); it is served at `/health`, outside `/v1/documents`.
219
+
220
+ ### Endpoints the SDK does not wrap
221
+
222
+ `HttpClient` / `AsyncHttpClient` are the transports behind the resources and apply the same headers, envelope unwrapping and error mapping (`request`, `request_void`, `request_text`, `request_binary`, `request_redirect`, `request_node_file`, `request_multipart`):
223
+
224
+ ```python
225
+ from lowcoai.docs import HttpClient
226
+
227
+ with HttpClient("<token>", org_id="org_123") as http:
228
+ data = http.request("GET", "/v1/documents/my-bucket/objects", query={"prefix": "a"})
229
+ ```
230
+
231
+ ## Errors
232
+
233
+ Every non-2xx response raises `DocsError`:
234
+
235
+ ```python
236
+ from lowcoai.docs import DocsError
237
+
238
+ try:
239
+ client.files.read(bucket, "missing.md")
240
+ except DocsError as err:
241
+ print(err.status, err.message, err.code, err.payload)
242
+ ```
243
+
244
+ - `message` — the best message in the error body. The service sends either `{"status": 0, "error": {"message", "code", "details"}}` or `{"message": "..."}`. When `error.message` is a platform code such as `AAS-00106`, `details` is the message and the code goes to `code`; otherwise `error.message` (or `message`) is used, falling back to `"Request failed with status <n>"`.
245
+ - `status` — the HTTP status; `0` when no response arrived (connection error, timeout — the `httpx` exception is chained as `__cause__`) or a `.` / `..` path segment was refused.
246
+ - `code` — the platform error code (e.g. `AAS-00106`) when the body carries one, else `None`.
247
+ - `payload` — the parsed JSON error body (a failed conditional save puts the file's current `currentEtag` / `updatedBy` under `payload["data"]`); the raw text when the body is not JSON; `None` when empty.
248
+
249
+ A 2xx response whose body is not JSON raises `DocsError` with that 2xx `status` and the raw text as `payload` (`triggers.delete` / `webhooks.delete` ignore the body). A redirect method that gets no 3xx `Location` raises `DocsError` too.
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "lowcoai-docs"
7
+ version = "0.1.0"
8
+ description = "Official Python client for the lowco document service (files, folders, sharing, triggers, webhooks)."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "httpx>=0.25,<1",
14
+ "typing-extensions>=4.7",
15
+ ]
16
+ keywords = ["documents", "files", "storage", "sharing", "lowco", "sdk", "client"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "Typing :: Typed",
20
+ "Framework :: AsyncIO",
21
+ ]
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/lowcoai/lowco-sdk"
25
+ Source = "https://github.com/lowcoai/lowco-sdk/tree/main/python/docs"
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=8", "pytest-asyncio>=0.23", "mypy>=1.10", "ruff>=0.5"]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/lowcoai"]
32
+
33
+ [tool.pytest.ini_options]
34
+ pythonpath = ["src"]
35
+ asyncio_mode = "auto"
36
+
37
+ [tool.mypy]
38
+ strict = true
39
+ mypy_path = "src"
40
+ explicit_package_bases = true
41
+ # Responses are decoded JSON typed as the documented TypedDict (like `as T` in
42
+ # the TypeScript SDK), so returning `Any` from endpoint methods is intended.
43
+ warn_return_any = false
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ target-version = "py310"
48
+
49
+ [tool.ruff.lint]
50
+ select = ["E", "F", "W", "I", "UP", "B"]
@@ -0,0 +1,23 @@
1
+ """Official Python client for the lowco document service (files, folders, sharing,
2
+ triggers, webhooks)."""
3
+
4
+ from . import types as _types
5
+ from ._base import API_PREFIX, BASE_URL
6
+ from ._client import AsyncDocsClient, DocsClient
7
+ from ._errors import DocsError
8
+ from ._http import AsyncHttpClient, HttpClient
9
+ from .types import * # noqa: F403
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ __all__ = [
14
+ "API_PREFIX",
15
+ "BASE_URL",
16
+ "AsyncDocsClient",
17
+ "AsyncHttpClient",
18
+ "DocsClient",
19
+ "DocsError",
20
+ "HttpClient",
21
+ "__version__",
22
+ ]
23
+ __all__ += _types.__all__