gushwork-argus 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,2 @@
1
+ .venv/
2
+ dist/
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.1.0 (2026-10-16)
6
+
7
+ First release, on PyPI.
8
+
9
+ - `Argus` and `AsyncArgus` clients configured by an API key and a base URL
10
+ (or `ARGUS_API_KEY` / `ARGUS_BASE_URL`).
11
+ - Namespaces: `create`, `get`, `find_by_slug`, `list`, `list_all`.
12
+ - Pages: `add` an HTML or Markdown page through a presigned S3 upload.
13
+ - Images: `add` a file (presigned S3 upload) or a URL, `list`, `list_all`, `search`.
14
+ - Ingestions: `get`, and `wait` until complete.
15
+ - Typed errors, retries with backoff on connection errors, timeouts, 429 and 502/503/504.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gushwork
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.5
2
+ Name: gushwork-argus
3
+ Version: 0.1.0
4
+ Summary: Python client for Argus: Gushwork's image ingestion and search API
5
+ Author: Gushwork
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: httpx<1,>=0.27
17
+ Requires-Dist: pydantic<3,>=2.6
18
+ Description-Content-Type: text/markdown
19
+
20
+ # gushwork-argus
21
+
22
+ Python client for **Argus**, Gushwork's image ingestion and search API. You give Argus
23
+ pages and images; it finds every image, describes it, and makes it searchable by meaning.
24
+
25
+ - One client, two settings: an API key and the API host.
26
+ - `Argus` (blocking) and `AsyncArgus` (asyncio) with the same methods.
27
+ - Typed responses (Pydantic v2), typed errors, retries with backoff.
28
+ - Python 3.10+. Depends only on `httpx` and `pydantic`.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install gushwork-argus
34
+ # or
35
+ uv add gushwork-argus
36
+ ```
37
+
38
+ Pin a version in services (`gushwork-argus==0.1.0`); see [CHANGELOG.md](CHANGELOG.md).
39
+
40
+ ## Configure
41
+
42
+ ```python
43
+ from gushwork_argus import Argus
44
+
45
+ argus = Argus(api_key="...", base_url="https://<argus host>")
46
+ ```
47
+
48
+ Or set `ARGUS_API_KEY` and `ARGUS_BASE_URL` and call `Argus()`. The Argus team gives you
49
+ both: a key with the scopes your service needs, and the host for each environment.
50
+
51
+ Optional: `timeout=30.0` (seconds per request), `max_retries=2`.
52
+ Create one client per process and reuse it; it keeps a connection pool. Close it with
53
+ `argus.close()` or use `with Argus(...) as argus:`.
54
+
55
+ ## Use
56
+
57
+ ```python
58
+ from pathlib import Path
59
+ from gushwork_argus import Argus, ConflictError
60
+
61
+ argus = Argus()
62
+
63
+ # 1. Create a namespace (admin key). Safe to retry with the same fields.
64
+ namespace = argus.namespaces.create(
65
+ slug="acme-dental", name="Acme Dental", site_name="Acme Dental", domain="acme.com"
66
+ )
67
+
68
+ # 2. Get it back, by id or slug
69
+ namespace = argus.namespaces.get(namespace.id)
70
+ namespace = argus.namespaces.find_by_slug("acme-dental") # None when there is none
71
+
72
+ # 3. Add a page or an image (ingest scope). Files go to S3 through a presigned upload.
73
+ page = argus.pages.add(namespace.id, Path("about.md"), page_url="https://acme.com/about")
74
+ page = argus.pages.add(namespace.id, html_text, type="html", page_url="https://acme.com/")
75
+ image = argus.images.add(namespace.id, file=Path("crown.jpg"), alt="Dentist fitting a crown")
76
+ image = argus.images.add(namespace.id, url="https://cdn.acme.com/crown.jpg") # no upload
77
+
78
+ # 7. Check an ingestion, or wait until it is complete
79
+ status = argus.ingestions.get(page.ingestion_id)
80
+ status = argus.ingestions.wait(page.ingestion_id, timeout=600)
81
+ print(status.status, status.images.searchable, "of", status.images.total, "searchable")
82
+
83
+ # 4. Search (retrieve scope)
84
+ response = argus.images.search(
85
+ namespace.id, "dentist fitting a crown", top_k=5, filters={"orientation": "landscape"}
86
+ )
87
+ for result in response.results:
88
+ print(result.rank, result.match, result.image.url, result.description)
89
+
90
+ # 5. List namespaces: one page, or all of them
91
+ page_of_namespaces = argus.namespaces.list(limit=50) # CursorPage: .items, .next_cursor
92
+ for ns in argus.namespaces.list_all():
93
+ print(ns.slug)
94
+
95
+ # 6. List images, newest first
96
+ for img in argus.images.list_all(namespace.id, status="RETRIEVAL_READY"):
97
+ print(img.media_id, img.description, img.file.url if img.file else None)
98
+ ```
99
+
100
+ ### Async
101
+
102
+ ```python
103
+ from gushwork_argus import AsyncArgus
104
+
105
+ async with AsyncArgus() as argus:
106
+ namespace = await argus.namespaces.get(namespace_id)
107
+ page = await argus.pages.add(namespace.id, Path("about.md"))
108
+ async for img in argus.images.list_all(namespace.id):
109
+ ...
110
+ ```
111
+
112
+ ### Pages and images, in detail
113
+
114
+ - `pages.add(namespace_id, content, *, type=None, page_url=None, page_title=None, markdown=None)`
115
+ `content` is a `Path` (type read from `.md`/`.html`), or the page itself as `str` or
116
+ `bytes` with `type="md"` or `type="html"`. **A `str` is always page text, never a file
117
+ name.** Send `page_url` whenever you have it: it resolves relative image links and tells
118
+ the describer where each image appears. For HTML, `markdown` (the same page converted to
119
+ markdown, if your scraper has it) improves captions. A page is identified by its bytes:
120
+ sending the same bytes again returns the first ingestion with `duplicate=True`, and any
121
+ new `page_url`, `page_title` or `markdown` is ignored.
122
+ - `images.add(namespace_id, *, file=None, url=None, alt=None, caption=None, page_url=None)`
123
+ Exactly one of `file` (a `Path`, `bytes` or a file opened `"rb"`) or `url`.
124
+ - An ingestion is `complete` when the page is parsed (or failed) and every image on it is
125
+ searchable or failed. Complete is not success: check `status`, `error` and
126
+ `images.failed`.
127
+ - Signed image links (`url`, `thumbnail_url`) expire after an hour. Store `media_id`, not
128
+ the link.
129
+
130
+ ## Errors
131
+
132
+ Every runtime error is an `ArgusError`. Bad arguments raise `ValueError` before any
133
+ request is sent.
134
+
135
+ | Exception | When |
136
+ |---|---|
137
+ | `APIConnectionError` / `APITimeoutError` | No answer from Argus (after retries) |
138
+ | `BadRequestError` (400) | Request refused, e.g. a search filter Argus can't apply |
139
+ | `AuthenticationError` (401) | API key missing, wrong or revoked |
140
+ | `PermissionDeniedError` (403) | Key lacks the scope, or the namespace is another tenant's |
141
+ | `NotFoundError` (404) | No such namespace or ingestion |
142
+ | `ConflictError` (409) | Slug taken with different fields (`.namespace_id`), or nothing searchable yet |
143
+ | `UnprocessableEntityError` (422) | A field has the wrong shape |
144
+ | `RateLimitError` (429) / `InternalServerError` (5xx) | After retries |
145
+ | `UploadError` | S3 refused the file |
146
+ | `APIResponseValidationError` | A 2xx body the SDK can't read (a proxy page, or a much newer server) |
147
+ | `IngestionTimeoutError` | `ingestions.wait()` ran out of time (`.ingestion` has the last status) |
148
+
149
+ Every `APIStatusError` has `status_code`, `detail` (the server's message, or a list of
150
+ field errors) and `body`.
151
+
152
+ ```python
153
+ try:
154
+ argus.namespaces.create(slug="acme-dental", name="Acme")
155
+ except ConflictError as exc:
156
+ namespace = argus.namespaces.get(exc.namespace_id)
157
+ ```
158
+
159
+ ## Retries
160
+
161
+ Writes are safe to repeat: namespaces are keyed by slug, pages by their content hash,
162
+ images by their source and context. So every call except search is retried on connection
163
+ errors, timeouts, 429, 502, 503 and 504, with exponential backoff and jitter
164
+ (`max_retries=2` by default; `0` turns retries off).
165
+
166
+ A search runs paid model calls each time, so it is retried only when it certainly never
167
+ reached Argus (a refused connection, or 429). A search that times out raises
168
+ `APITimeoutError` instead of running again; raise `timeout` if accurate searches on large
169
+ namespaces need longer. `ingestions.wait()` keeps polling through connection errors and
170
+ 5xx until its own `timeout`.
171
+
172
+ Retries are logged at INFO on the `gushwork_argus` logger.
173
+
174
+ S3 uploads go through a separate HTTP client that never carries the API key. It does not
175
+ use a custom `http_client`; like any httpx client it reads `HTTPS_PROXY` and
176
+ `SSL_CERT_FILE` from the environment.
177
+
178
+ ## API keys and scopes
179
+
180
+ | Scope | Can |
181
+ |---|---|
182
+ | `admin` | create and edit namespaces, read anything |
183
+ | `ingest` | add pages and images, check ingestions |
184
+ | `retrieve` | search |
185
+
186
+ Any key can read namespaces and list images. A key bound to a namespace sees only that
187
+ namespace.
188
+
189
+ ## Develop
190
+
191
+ See [CLAUDE.md](CLAUDE.md) for the layout, conventions and the release process.
192
+
193
+ ```bash
194
+ cd sdk/python
195
+ uv sync
196
+ uv run pytest
197
+ uv run ruff check . && uv run ruff format --check . && uv run pyright
198
+ ```
@@ -0,0 +1,179 @@
1
+ # gushwork-argus
2
+
3
+ Python client for **Argus**, Gushwork's image ingestion and search API. You give Argus
4
+ pages and images; it finds every image, describes it, and makes it searchable by meaning.
5
+
6
+ - One client, two settings: an API key and the API host.
7
+ - `Argus` (blocking) and `AsyncArgus` (asyncio) with the same methods.
8
+ - Typed responses (Pydantic v2), typed errors, retries with backoff.
9
+ - Python 3.10+. Depends only on `httpx` and `pydantic`.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install gushwork-argus
15
+ # or
16
+ uv add gushwork-argus
17
+ ```
18
+
19
+ Pin a version in services (`gushwork-argus==0.1.0`); see [CHANGELOG.md](CHANGELOG.md).
20
+
21
+ ## Configure
22
+
23
+ ```python
24
+ from gushwork_argus import Argus
25
+
26
+ argus = Argus(api_key="...", base_url="https://<argus host>")
27
+ ```
28
+
29
+ Or set `ARGUS_API_KEY` and `ARGUS_BASE_URL` and call `Argus()`. The Argus team gives you
30
+ both: a key with the scopes your service needs, and the host for each environment.
31
+
32
+ Optional: `timeout=30.0` (seconds per request), `max_retries=2`.
33
+ Create one client per process and reuse it; it keeps a connection pool. Close it with
34
+ `argus.close()` or use `with Argus(...) as argus:`.
35
+
36
+ ## Use
37
+
38
+ ```python
39
+ from pathlib import Path
40
+ from gushwork_argus import Argus, ConflictError
41
+
42
+ argus = Argus()
43
+
44
+ # 1. Create a namespace (admin key). Safe to retry with the same fields.
45
+ namespace = argus.namespaces.create(
46
+ slug="acme-dental", name="Acme Dental", site_name="Acme Dental", domain="acme.com"
47
+ )
48
+
49
+ # 2. Get it back, by id or slug
50
+ namespace = argus.namespaces.get(namespace.id)
51
+ namespace = argus.namespaces.find_by_slug("acme-dental") # None when there is none
52
+
53
+ # 3. Add a page or an image (ingest scope). Files go to S3 through a presigned upload.
54
+ page = argus.pages.add(namespace.id, Path("about.md"), page_url="https://acme.com/about")
55
+ page = argus.pages.add(namespace.id, html_text, type="html", page_url="https://acme.com/")
56
+ image = argus.images.add(namespace.id, file=Path("crown.jpg"), alt="Dentist fitting a crown")
57
+ image = argus.images.add(namespace.id, url="https://cdn.acme.com/crown.jpg") # no upload
58
+
59
+ # 7. Check an ingestion, or wait until it is complete
60
+ status = argus.ingestions.get(page.ingestion_id)
61
+ status = argus.ingestions.wait(page.ingestion_id, timeout=600)
62
+ print(status.status, status.images.searchable, "of", status.images.total, "searchable")
63
+
64
+ # 4. Search (retrieve scope)
65
+ response = argus.images.search(
66
+ namespace.id, "dentist fitting a crown", top_k=5, filters={"orientation": "landscape"}
67
+ )
68
+ for result in response.results:
69
+ print(result.rank, result.match, result.image.url, result.description)
70
+
71
+ # 5. List namespaces: one page, or all of them
72
+ page_of_namespaces = argus.namespaces.list(limit=50) # CursorPage: .items, .next_cursor
73
+ for ns in argus.namespaces.list_all():
74
+ print(ns.slug)
75
+
76
+ # 6. List images, newest first
77
+ for img in argus.images.list_all(namespace.id, status="RETRIEVAL_READY"):
78
+ print(img.media_id, img.description, img.file.url if img.file else None)
79
+ ```
80
+
81
+ ### Async
82
+
83
+ ```python
84
+ from gushwork_argus import AsyncArgus
85
+
86
+ async with AsyncArgus() as argus:
87
+ namespace = await argus.namespaces.get(namespace_id)
88
+ page = await argus.pages.add(namespace.id, Path("about.md"))
89
+ async for img in argus.images.list_all(namespace.id):
90
+ ...
91
+ ```
92
+
93
+ ### Pages and images, in detail
94
+
95
+ - `pages.add(namespace_id, content, *, type=None, page_url=None, page_title=None, markdown=None)`
96
+ `content` is a `Path` (type read from `.md`/`.html`), or the page itself as `str` or
97
+ `bytes` with `type="md"` or `type="html"`. **A `str` is always page text, never a file
98
+ name.** Send `page_url` whenever you have it: it resolves relative image links and tells
99
+ the describer where each image appears. For HTML, `markdown` (the same page converted to
100
+ markdown, if your scraper has it) improves captions. A page is identified by its bytes:
101
+ sending the same bytes again returns the first ingestion with `duplicate=True`, and any
102
+ new `page_url`, `page_title` or `markdown` is ignored.
103
+ - `images.add(namespace_id, *, file=None, url=None, alt=None, caption=None, page_url=None)`
104
+ Exactly one of `file` (a `Path`, `bytes` or a file opened `"rb"`) or `url`.
105
+ - An ingestion is `complete` when the page is parsed (or failed) and every image on it is
106
+ searchable or failed. Complete is not success: check `status`, `error` and
107
+ `images.failed`.
108
+ - Signed image links (`url`, `thumbnail_url`) expire after an hour. Store `media_id`, not
109
+ the link.
110
+
111
+ ## Errors
112
+
113
+ Every runtime error is an `ArgusError`. Bad arguments raise `ValueError` before any
114
+ request is sent.
115
+
116
+ | Exception | When |
117
+ |---|---|
118
+ | `APIConnectionError` / `APITimeoutError` | No answer from Argus (after retries) |
119
+ | `BadRequestError` (400) | Request refused, e.g. a search filter Argus can't apply |
120
+ | `AuthenticationError` (401) | API key missing, wrong or revoked |
121
+ | `PermissionDeniedError` (403) | Key lacks the scope, or the namespace is another tenant's |
122
+ | `NotFoundError` (404) | No such namespace or ingestion |
123
+ | `ConflictError` (409) | Slug taken with different fields (`.namespace_id`), or nothing searchable yet |
124
+ | `UnprocessableEntityError` (422) | A field has the wrong shape |
125
+ | `RateLimitError` (429) / `InternalServerError` (5xx) | After retries |
126
+ | `UploadError` | S3 refused the file |
127
+ | `APIResponseValidationError` | A 2xx body the SDK can't read (a proxy page, or a much newer server) |
128
+ | `IngestionTimeoutError` | `ingestions.wait()` ran out of time (`.ingestion` has the last status) |
129
+
130
+ Every `APIStatusError` has `status_code`, `detail` (the server's message, or a list of
131
+ field errors) and `body`.
132
+
133
+ ```python
134
+ try:
135
+ argus.namespaces.create(slug="acme-dental", name="Acme")
136
+ except ConflictError as exc:
137
+ namespace = argus.namespaces.get(exc.namespace_id)
138
+ ```
139
+
140
+ ## Retries
141
+
142
+ Writes are safe to repeat: namespaces are keyed by slug, pages by their content hash,
143
+ images by their source and context. So every call except search is retried on connection
144
+ errors, timeouts, 429, 502, 503 and 504, with exponential backoff and jitter
145
+ (`max_retries=2` by default; `0` turns retries off).
146
+
147
+ A search runs paid model calls each time, so it is retried only when it certainly never
148
+ reached Argus (a refused connection, or 429). A search that times out raises
149
+ `APITimeoutError` instead of running again; raise `timeout` if accurate searches on large
150
+ namespaces need longer. `ingestions.wait()` keeps polling through connection errors and
151
+ 5xx until its own `timeout`.
152
+
153
+ Retries are logged at INFO on the `gushwork_argus` logger.
154
+
155
+ S3 uploads go through a separate HTTP client that never carries the API key. It does not
156
+ use a custom `http_client`; like any httpx client it reads `HTTPS_PROXY` and
157
+ `SSL_CERT_FILE` from the environment.
158
+
159
+ ## API keys and scopes
160
+
161
+ | Scope | Can |
162
+ |---|---|
163
+ | `admin` | create and edit namespaces, read anything |
164
+ | `ingest` | add pages and images, check ingestions |
165
+ | `retrieve` | search |
166
+
167
+ Any key can read namespaces and list images. A key bound to a namespace sees only that
168
+ namespace.
169
+
170
+ ## Develop
171
+
172
+ See [CLAUDE.md](CLAUDE.md) for the layout, conventions and the release process.
173
+
174
+ ```bash
175
+ cd sdk/python
176
+ uv sync
177
+ uv run pytest
178
+ uv run ruff check . && uv run ruff format --check . && uv run pyright
179
+ ```
@@ -0,0 +1,60 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "gushwork-argus"
7
+ dynamic = ["version"]
8
+ description = "Python client for Argus: Gushwork's image ingestion and search API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Gushwork" }]
13
+ dependencies = [
14
+ "httpx>=0.27,<1",
15
+ "pydantic>=2.6,<3",
16
+ ]
17
+ classifiers = [
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Typing :: Typed",
25
+ ]
26
+
27
+ [tool.hatch.version]
28
+ path = "src/gushwork_argus/_version.py"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/gushwork_argus"]
32
+
33
+ # What PyPI gets as the source distribution: the package, not the tests or contributor notes.
34
+ [tool.hatch.build.targets.sdist]
35
+ include = ["src/gushwork_argus", "README.md", "CHANGELOG.md", "LICENSE"]
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pyright>=1.1.390",
40
+ "pytest>=8.3",
41
+ "pytest-asyncio>=0.24",
42
+ "respx>=0.22",
43
+ "ruff>=0.8",
44
+ ]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ asyncio_mode = "auto"
49
+
50
+ [tool.ruff]
51
+ line-length = 100
52
+ target-version = "py310"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
56
+
57
+ [tool.pyright]
58
+ include = ["src"]
59
+ pythonVersion = "3.10"
60
+ typeCheckingMode = "strict"
@@ -0,0 +1,78 @@
1
+ """Python client for Argus, Gushwork's image ingestion and search API."""
2
+
3
+ from ._client import Argus, AsyncArgus
4
+ from ._version import __version__
5
+ from .errors import (
6
+ APIConnectionError,
7
+ APIResponseValidationError,
8
+ APIStatusError,
9
+ APITimeoutError,
10
+ ArgusError,
11
+ AuthenticationError,
12
+ BadRequestError,
13
+ ConflictError,
14
+ IngestionTimeoutError,
15
+ InternalServerError,
16
+ NotFoundError,
17
+ PermissionDeniedError,
18
+ RateLimitError,
19
+ UnprocessableEntityError,
20
+ UploadError,
21
+ )
22
+ from .models import (
23
+ CursorPage,
24
+ DoorMatches,
25
+ Image,
26
+ ImageAdded,
27
+ ImageFile,
28
+ ImageProgress,
29
+ ImageStatus,
30
+ IndexState,
31
+ Ingestion,
32
+ Namespace,
33
+ PageAdded,
34
+ PageType,
35
+ SearchFilters,
36
+ SearchMode,
37
+ SearchResponse,
38
+ SearchResult,
39
+ Usage,
40
+ )
41
+
42
+ __all__ = [
43
+ "APIConnectionError",
44
+ "APIResponseValidationError",
45
+ "APIStatusError",
46
+ "APITimeoutError",
47
+ "Argus",
48
+ "ArgusError",
49
+ "AsyncArgus",
50
+ "AuthenticationError",
51
+ "BadRequestError",
52
+ "ConflictError",
53
+ "CursorPage",
54
+ "DoorMatches",
55
+ "Image",
56
+ "ImageAdded",
57
+ "ImageFile",
58
+ "ImageProgress",
59
+ "ImageStatus",
60
+ "IndexState",
61
+ "Ingestion",
62
+ "IngestionTimeoutError",
63
+ "InternalServerError",
64
+ "Namespace",
65
+ "NotFoundError",
66
+ "PageAdded",
67
+ "PageType",
68
+ "PermissionDeniedError",
69
+ "RateLimitError",
70
+ "SearchFilters",
71
+ "SearchMode",
72
+ "SearchResponse",
73
+ "SearchResult",
74
+ "UnprocessableEntityError",
75
+ "UploadError",
76
+ "Usage",
77
+ "__version__",
78
+ ]
@@ -0,0 +1,88 @@
1
+ """The two entry points: Argus (blocking) and AsyncArgus (asyncio). Same methods on both."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ from ._http import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, AsyncTransport, Settings, SyncTransport
8
+ from .resources import (
9
+ AsyncImages,
10
+ AsyncIngestions,
11
+ AsyncNamespaces,
12
+ AsyncPages,
13
+ Images,
14
+ Ingestions,
15
+ Namespaces,
16
+ Pages,
17
+ )
18
+
19
+
20
+ class Argus:
21
+ """A client for the Argus API.
22
+
23
+ argus = Argus(api_key="...", base_url="https://<argus host>")
24
+ namespace = argus.namespaces.create(slug="acme", name="Acme Dental")
25
+
26
+ `api_key` and `base_url` fall back to ARGUS_API_KEY and ARGUS_BASE_URL. `timeout` is
27
+ per request, in seconds. `max_retries` covers connection errors, timeouts, 429 and
28
+ 502/503/504. Pass `http_client` only for proxies or custom transports. Reuse one client
29
+ per process; it holds a connection pool. Close it with `close()` or a `with` block.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ api_key: str | None = None,
35
+ base_url: str | None = None,
36
+ *,
37
+ timeout: float = DEFAULT_TIMEOUT,
38
+ max_retries: int = DEFAULT_MAX_RETRIES,
39
+ http_client: httpx.Client | None = None,
40
+ ) -> None:
41
+ settings = Settings.resolve(api_key, base_url, timeout, max_retries)
42
+ self._transport = SyncTransport(settings, http_client)
43
+ self.namespaces = Namespaces(self._transport)
44
+ self.pages = Pages(self._transport)
45
+ self.images = Images(self._transport)
46
+ self.ingestions = Ingestions(self._transport)
47
+
48
+ def close(self) -> None:
49
+ self._transport.close()
50
+
51
+ def __enter__(self) -> Argus:
52
+ return self
53
+
54
+ def __exit__(self, *exc_info: object) -> None:
55
+ self.close()
56
+
57
+
58
+ class AsyncArgus:
59
+ """The asyncio client: the same methods as `Argus`, awaited.
60
+
61
+ async with AsyncArgus(api_key="...", base_url="https://<argus host>") as argus:
62
+ namespace = await argus.namespaces.get(namespace_id)
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ api_key: str | None = None,
68
+ base_url: str | None = None,
69
+ *,
70
+ timeout: float = DEFAULT_TIMEOUT,
71
+ max_retries: int = DEFAULT_MAX_RETRIES,
72
+ http_client: httpx.AsyncClient | None = None,
73
+ ) -> None:
74
+ settings = Settings.resolve(api_key, base_url, timeout, max_retries)
75
+ self._transport = AsyncTransport(settings, http_client)
76
+ self.namespaces = AsyncNamespaces(self._transport)
77
+ self.pages = AsyncPages(self._transport)
78
+ self.images = AsyncImages(self._transport)
79
+ self.ingestions = AsyncIngestions(self._transport)
80
+
81
+ async def close(self) -> None:
82
+ await self._transport.close()
83
+
84
+ async def __aenter__(self) -> AsyncArgus:
85
+ return self
86
+
87
+ async def __aexit__(self, *exc_info: object) -> None:
88
+ await self.close()