saia-python 0.4.1__py3-none-any.whl

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,70 @@
1
+ """OpenAI SDK compatibility layer for the SAIA platform.
2
+
3
+ Provides a factory function that creates an ``openai.OpenAI`` (or
4
+ ``openai.AsyncOpenAI``) client pre-configured with SAIA credentials
5
+ and base URL. This enables direct use of the OpenAI Python SDK and
6
+ tools built on it (RAGAS, LangChain, instructor, etc.) against the
7
+ SAIA API.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+
13
+ def create_openai_client(
14
+ *,
15
+ api_key: str | None = None,
16
+ base_url: str | None = None,
17
+ key_file: str | None = None,
18
+ async_client: bool = False,
19
+ ):
20
+ """Create an OpenAI client configured for the SAIA platform.
21
+
22
+ Reuses the same credential and base URL resolution as
23
+ :class:`~saia_python.SAIAClient`: environment variables, ``.env``,
24
+ ``.saia_api``, and ``config.toml`` are checked automatically when
25
+ parameters are omitted.
26
+
27
+ Args:
28
+ api_key: Explicit API key. If omitted, resolved via
29
+ :func:`~saia_python.load_api_key`.
30
+ base_url: Explicit base URL. If omitted, resolved via
31
+ :func:`~saia_python.resolve_base_url`.
32
+ key_file: Path to a ``.saia_api`` or ``.env`` file. Ignored
33
+ when ``api_key`` is provided.
34
+ async_client: If ``True``, return an ``openai.AsyncOpenAI``
35
+ instance instead of ``openai.OpenAI``.
36
+
37
+ Returns:
38
+ An ``openai.OpenAI`` or ``openai.AsyncOpenAI`` instance.
39
+
40
+ Example::
41
+
42
+ from saia_python import create_openai_client
43
+
44
+ client = create_openai_client()
45
+ response = client.chat.completions.create(
46
+ model="llama-3.3-70b-instruct",
47
+ messages=[{"role": "user", "content": "Hello!"}],
48
+ )
49
+
50
+ # Embeddings
51
+ embedding = client.embeddings.create(
52
+ model="e5-mistral-7b-instruct",
53
+ input="Text to embed",
54
+ )
55
+ """
56
+ try:
57
+ import openai
58
+ except ImportError as exc:
59
+ raise ImportError(
60
+ "The OpenAI compatibility layer requires the optional 'openai' "
61
+ "dependency. Install it with:\n"
62
+ " pip install saia-python[openai]"
63
+ ) from exc
64
+
65
+ from .auth import resolve_credentials
66
+
67
+ resolved_key, resolved_url = resolve_credentials(api_key, base_url, key_file)
68
+
69
+ cls = openai.AsyncOpenAI if async_client else openai.OpenAI
70
+ return cls(api_key=resolved_key, base_url=resolved_url)
saia_python/py.typed ADDED
File without changes
@@ -0,0 +1,84 @@
1
+ """Rate limit header parsing for SAIA API responses."""
2
+
3
+ from dataclasses import asdict, dataclass
4
+
5
+
6
+ @dataclass
7
+ class RateLimitInfo:
8
+ """Parsed rate-limit information from SAIA API response headers."""
9
+
10
+ limit_minute: int | None = None
11
+ limit_hour: int | None = None
12
+ limit_day: int | None = None
13
+ limit_month: int | None = None
14
+ remaining_minute: int | None = None
15
+ remaining_hour: int | None = None
16
+ remaining_day: int | None = None
17
+ remaining_month: int | None = None
18
+ reset_seconds: int | None = None
19
+
20
+ def to_dict(self) -> dict:
21
+ """Return a plain, JSON-serializable dict of all rate-limit fields."""
22
+ return asdict(self)
23
+
24
+ def __str__(self):
25
+ rows = []
26
+ for window in ("minute", "hour", "day", "month"):
27
+ limit = getattr(self, f"limit_{window}")
28
+ remaining = getattr(self, f"remaining_{window}")
29
+ if limit is not None:
30
+ rem_str = str(remaining) if remaining is not None else "?"
31
+ used = str(limit - remaining) if remaining is not None else "?"
32
+ rows.append((window, rem_str, limit, used))
33
+
34
+ if not rows:
35
+ return "SAIA Rate Limits: (no data)"
36
+
37
+ w_rem = max(len(str(r[1])) for r in rows)
38
+ w_lim = max(len(str(r[2])) for r in rows)
39
+ w_used = max(len(str(r[3])) for r in rows)
40
+
41
+ lines = ["SAIA Rate Limits:"]
42
+ for window, remaining, limit, used in rows:
43
+ lines.append(
44
+ f" {window:<6} "
45
+ f"{remaining:>{w_rem}} / {limit:>{w_lim}} "
46
+ f"remaining "
47
+ f"({used:>{w_used}} used)"
48
+ )
49
+ if self.reset_seconds is not None:
50
+ lines.append(f" Resets in {self.reset_seconds}s")
51
+ return "\n".join(lines)
52
+
53
+
54
+ _HEADER_MAP = {
55
+ "x-ratelimit-limit-minute": "limit_minute",
56
+ "x-ratelimit-limit-hour": "limit_hour",
57
+ "x-ratelimit-limit-day": "limit_day",
58
+ "x-ratelimit-limit-month": "limit_month",
59
+ "x-ratelimit-remaining-minute": "remaining_minute",
60
+ "x-ratelimit-remaining-hour": "remaining_hour",
61
+ "x-ratelimit-remaining-day": "remaining_day",
62
+ "x-ratelimit-remaining-month": "remaining_month",
63
+ "ratelimit-reset": "reset_seconds",
64
+ }
65
+
66
+
67
+ def parse_rate_limits(headers) -> RateLimitInfo:
68
+ """Parse rate-limit info from HTTP response headers.
69
+
70
+ Args:
71
+ headers: A dict-like object (e.g. ``requests.Response.headers``).
72
+
73
+ Returns:
74
+ RateLimitInfo with populated fields for each header found.
75
+ """
76
+ kwargs = {}
77
+ for header_name, field_name in _HEADER_MAP.items():
78
+ value = headers.get(header_name)
79
+ if value is not None:
80
+ try:
81
+ kwargs[field_name] = int(value)
82
+ except (ValueError, TypeError):
83
+ pass
84
+ return RateLimitInfo(**kwargs)
@@ -0,0 +1,70 @@
1
+ """Helpers for extracting fields from OpenAI-style API responses.
2
+
3
+ The SAIA Chat AI and ARCANA RAG endpoints both return the canonical
4
+ OpenAI ``ChatCompletion`` envelope::
5
+
6
+ {
7
+ "choices": [
8
+ {"message": {"role": "assistant", "content": "<text>"}, ...}
9
+ ],
10
+ ...
11
+ }
12
+
13
+ Every caller eventually writes the same lookup chain. The helpers in
14
+ this module make that lookup safe and uniform — empty ``choices`` lists
15
+ and missing ``content`` fields return ``""`` (with a logged warning) so
16
+ downstream string-handling code doesn't have to special-case them.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ def text_of(response: dict) -> str:
27
+ """Extract the assistant message content from a chat / RAG response.
28
+
29
+ Reaches into ``response["choices"][0]["message"]["content"]`` with
30
+ full nil-safety: an empty ``choices`` list, a missing ``message``
31
+ key, or a ``None`` ``content`` field all collapse to ``""`` rather
32
+ than raising. Both empty-response cases log a warning at this
33
+ module's logger so silent regressions surface in logs.
34
+
35
+ Works for the response shape returned by:
36
+
37
+ - :meth:`saia_python.ChatService.completions`
38
+ - :meth:`saia_python.ArcanaService.chat`
39
+ - ``client.openai.chat.completions.create(...).model_dump()``
40
+
41
+ Args:
42
+ response: An OpenAI-style ChatCompletion response dict.
43
+
44
+ Returns:
45
+ The first choice's assistant content string, or ``""`` if the
46
+ response carries no usable content.
47
+
48
+ Example::
49
+
50
+ resp = client.arcana.chat(
51
+ model="...", messages=[...], arcana_id="...",
52
+ )
53
+ answer = saia_python.text_of(resp)
54
+ """
55
+ choices = response.get("choices") or []
56
+ if not choices:
57
+ log.warning(
58
+ "text_of: response has no choices; keys=%s",
59
+ list(response.keys()),
60
+ )
61
+ return ""
62
+ message = choices[0].get("message") or {}
63
+ content = message.get("content")
64
+ if not content:
65
+ log.warning(
66
+ "text_of: first choice has empty content; finish_reason=%r",
67
+ choices[0].get("finish_reason"),
68
+ )
69
+ return ""
70
+ return str(content)
saia_python/voice.py ADDED
@@ -0,0 +1,175 @@
1
+ """Voice AI service — transcription and translation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import concurrent.futures
6
+ import threading
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from ._http import new_session_like
11
+ from .exceptions import raise_for_status
12
+
13
+ if TYPE_CHECKING:
14
+ import requests
15
+
16
+
17
+ class VoiceService:
18
+ """Access the ``/audio/transcriptions`` and ``/audio/translations`` endpoints.
19
+
20
+ Args:
21
+ session: A :class:`requests.Session` with auth headers configured.
22
+ base_url: The SAIA API base URL.
23
+ """
24
+
25
+ def __init__(self, session: requests.Session, base_url: str):
26
+ self._session = session
27
+ self._base_url = base_url
28
+
29
+ def transcribe(
30
+ self,
31
+ file_path: str | Path,
32
+ *,
33
+ model: str = "whisper-large-v2",
34
+ response_format: str = "text",
35
+ language: str | None = None,
36
+ wait: bool = True,
37
+ ) -> str | concurrent.futures.Future[str]:
38
+ """Transcribe an audio file to text.
39
+
40
+ Args:
41
+ file_path: Path to the audio file (WAV, MP3, MP4, FLAC).
42
+ model: Whisper model to use.
43
+ response_format: Output format — ``"text"``, ``"vtt"``, or ``"srt"``.
44
+ language: Optional language hint (e.g. ``"de"``, ``"en"``).
45
+ wait: If ``True`` (default), block until the transcription is
46
+ ready and return it as a string. If ``False``, submit the
47
+ request on a background thread and return a
48
+ :class:`concurrent.futures.Future` immediately so the call
49
+ does not block. Resolve it with ``.result()`` (which
50
+ re-raises any error), poll with ``.done()``, or attach
51
+ ``.add_done_callback(...)``.
52
+
53
+ Returns:
54
+ The transcription string when ``wait=True``; otherwise a
55
+ ``Future`` that resolves to that string.
56
+
57
+ Example::
58
+
59
+ # Blocking
60
+ text = client.voice.transcribe("meeting.mp3")
61
+
62
+ # Non-blocking — kick it off, do other work, collect later
63
+ fut = client.voice.transcribe("meeting.mp3", wait=False)
64
+ ... # other work runs while it transcribes
65
+ text = fut.result() # blocks only now, re-raises on error
66
+ """
67
+ return self._audio_request(
68
+ "/audio/transcriptions",
69
+ file_path,
70
+ model=model,
71
+ response_format=response_format,
72
+ language=language,
73
+ wait=wait,
74
+ )
75
+
76
+ def translate(
77
+ self,
78
+ file_path: str | Path,
79
+ *,
80
+ model: str = "whisper-large-v2",
81
+ response_format: str = "text",
82
+ wait: bool = True,
83
+ ) -> str | concurrent.futures.Future[str]:
84
+ """Translate an audio file to English text.
85
+
86
+ Args:
87
+ file_path: Path to the audio file (WAV, MP3, MP4, FLAC).
88
+ model: Whisper model to use.
89
+ response_format: Output format — ``"text"``, ``"vtt"``, or ``"srt"``.
90
+ wait: If ``True`` (default), block until the result is ready and
91
+ return it as a string. If ``False``, submit on a background
92
+ thread and return a :class:`concurrent.futures.Future` that
93
+ resolves to the translation (see :meth:`transcribe`).
94
+
95
+ Returns:
96
+ The English translation string when ``wait=True``; otherwise a
97
+ ``Future`` that resolves to that string.
98
+ """
99
+ return self._audio_request(
100
+ "/audio/translations",
101
+ file_path,
102
+ model=model,
103
+ response_format=response_format,
104
+ wait=wait,
105
+ )
106
+
107
+ def _new_session(self) -> requests.Session:
108
+ """Create a fresh Session mirroring the client's auth headers.
109
+
110
+ Backgrounded requests (``wait=False``) use their own Session so they
111
+ never share the client's Session — and its connection pool — across
112
+ threads (``requests.Session`` is not guaranteed thread-safe). Thin
113
+ wrapper around :func:`saia_python._http.new_session_like` so the
114
+ "fresh authed background Session" idiom has a single implementation.
115
+ """
116
+ return new_session_like(self._session)
117
+
118
+ def _audio_request(
119
+ self,
120
+ endpoint: str,
121
+ file_path: str | Path,
122
+ *,
123
+ model: str,
124
+ response_format: str,
125
+ language: str | None = None,
126
+ wait: bool = True,
127
+ ) -> str | concurrent.futures.Future[str]:
128
+ file_path = Path(file_path)
129
+
130
+ def _send(session: requests.Session) -> str:
131
+ data = {"model": model, "response_format": response_format}
132
+ if language:
133
+ data["language"] = language
134
+
135
+ with open(file_path, "rb") as f:
136
+ resp = session.post(
137
+ f"{self._base_url}{endpoint}",
138
+ data=data,
139
+ files={"file": (file_path.name, f)},
140
+ )
141
+ raise_for_status(resp)
142
+
143
+ content_type = resp.headers.get("content-type", "")
144
+ if "json" in content_type:
145
+ body = resp.json()
146
+ if isinstance(body, dict):
147
+ return body.get("text", resp.text)
148
+ return str(body)
149
+ return resp.text
150
+
151
+ if wait:
152
+ return _send(self._session)
153
+
154
+ # Non-blocking: run on a worker thread with its OWN Session and hand
155
+ # back a Future. The caller resolves it with .result() (which
156
+ # re-raises any error), polls with .done(), or attaches a callback —
157
+ # so the result is never lost the way a bare fire-and-forget loses it.
158
+ future: concurrent.futures.Future[str] = concurrent.futures.Future()
159
+
160
+ def _worker() -> None:
161
+ if not future.set_running_or_notify_cancel():
162
+ return
163
+ session = self._new_session()
164
+ try:
165
+ future.set_result(_send(session))
166
+ except BaseException as exc: # surfaced to the caller via .result()
167
+ future.set_exception(exc)
168
+ finally:
169
+ session.close()
170
+
171
+ threading.Thread(target=_worker, daemon=True).start()
172
+ return future
173
+
174
+ def __repr__(self):
175
+ return f"VoiceService(base_url={self._base_url!r})"
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: saia-python
3
+ Version: 0.4.1
4
+ Summary: Python wrapper for the GWDG SAIA platform REST API
5
+ Author: Friedrich Schwarz
6
+ License-Expression: AGPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/fschwar4/saia_python
8
+ Project-URL: Documentation, https://fschwar4.github.io/saia_python/
9
+ Project-URL: Repository, https://github.com/fschwar4/saia_python
10
+ Project-URL: Changelog, https://github.com/fschwar4/saia_python/blob/main/docs/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/fschwar4/saia_python/issues
12
+ Keywords: GWDG,SAIA,ARCANA,RAG,LLM,OpenAI,academic-cloud,whisper,AI
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: requests>=2.28
30
+ Requires-Dist: tqdm>=4.60
31
+ Requires-Dist: tomlkit>=0.12
32
+ Provides-Extra: openai
33
+ Requires-Dist: openai>=1.0; extra == "openai"
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest>=7.0; extra == "test"
36
+ Requires-Dist: pytest-cov>=4.0; extra == "test"
37
+ Requires-Dist: saia-python[openai]; extra == "test"
38
+ Provides-Extra: docs
39
+ Requires-Dist: sphinx>=7.0; extra == "docs"
40
+ Requires-Dist: pydata-sphinx-theme>=0.15; extra == "docs"
41
+ Requires-Dist: sphinx-design; extra == "docs"
42
+ Requires-Dist: sphinx-copybutton; extra == "docs"
43
+ Requires-Dist: myst-parser; extra == "docs"
44
+ Provides-Extra: lint
45
+ Requires-Dist: ruff>=0.6; extra == "lint"
46
+ Requires-Dist: mypy>=1.10; extra == "lint"
47
+ Requires-Dist: types-requests; extra == "lint"
48
+ Provides-Extra: dev
49
+ Requires-Dist: saia-python[docs,lint,test]; extra == "dev"
50
+ Requires-Dist: jupyter; extra == "dev"
51
+ Dynamic: license-file
52
+
53
+ # saia-python
54
+
55
+ [![PyPI](https://img.shields.io/pypi/v/saia-python.svg)](https://pypi.org/project/saia-python/)
56
+ [![Python versions](https://img.shields.io/pypi/pyversions/saia-python.svg)](https://pypi.org/project/saia-python/)
57
+ [![License: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-blue.svg)](https://github.com/fschwar4/saia_python/blob/main/LICENSE)
58
+ [![Tests](https://github.com/fschwar4/saia_python/actions/workflows/tests.yml/badge.svg)](https://github.com/fschwar4/saia_python/actions/workflows/tests.yml)
59
+ [![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://fschwar4.github.io/saia_python/)
60
+ <!-- After enabling Zenodo (Settings → Integrations → GitHub) and cutting a release,
61
+ paste the DOI badge Zenodo provides, e.g.:
62
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.XXXXXXX.svg)](https://doi.org/10.5281/zenodo.XXXXXXX) -->
63
+
64
+ A Python wrapper for the [GWDG SAIA (Scalable AI Accelerator) platform](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) REST API.
65
+
66
+ SAIA provides self-hosted, OpenAI-compatible AI services at GWDG, including chat completions, voice transcription/translation, document conversion, and RAG (ARCANA). This library wraps the REST API so you can use it from Python — both as an object-oriented client and as standalone functions.
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ pip install saia-python
72
+ ```
73
+
74
+ Or from source:
75
+
76
+ ```bash
77
+ git clone https://github.com/fschwar4/saia_python.git
78
+ cd saia_python
79
+ pip install -e .
80
+ ```
81
+
82
+ ## Quick Start
83
+
84
+ ```python
85
+ from saia_python import SAIAClient
86
+
87
+ # API key auto-discovered from SAIA_API_KEY env var, .saia_api, or .env file
88
+ client = SAIAClient()
89
+
90
+ # List available models
91
+ print(client.models.list_ids())
92
+
93
+ # Chat completion
94
+ response = client.chat.completions(
95
+ model="meta-llama-3.1-8b-instruct",
96
+ messages=[{"role": "user", "content": "Hello!"}],
97
+ )
98
+ print(response["choices"][0]["message"]["content"])
99
+
100
+ # Check your rate limits
101
+ print(client.get_rate_limits())
102
+ ```
103
+
104
+ All services are also available as standalone functions:
105
+
106
+ ```python
107
+ from saia_python import list_model_ids, chat_completion
108
+
109
+ list_model_ids()
110
+ chat_completion(model="meta-llama-3.1-8b-instruct", messages=[...])
111
+ ```
112
+
113
+ ## Supported Services
114
+
115
+ | Service | Description | GWDG Docs |
116
+ |---------|-------------|-----------|
117
+ | **Chat AI** | Chat completions with streaming and tool calling | [Chat AI](https://docs.hpc.gwdg.de/services/ai-services/chat-ai/index.html) |
118
+ | **Voice AI** | Audio transcription and translation (Whisper) | [Voice AI](https://docs.hpc.gwdg.de/services/ai-services/voice-ai/index.html) |
119
+ | **ARCANA** | RAG — knowledge base management and retrieval-augmented chat | [ARCANA](https://docs.hpc.gwdg.de/services/ai-services/arcana/index.html) |
120
+ | **Documents** | PDF/document conversion via Docling | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
121
+ | **Models** | List available models, probe tool-calling support | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
122
+ | **Rate Limits** | Inspect current quota and usage | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
123
+
124
+ ## Repository Structure
125
+
126
+ ```
127
+ saia-python/
128
+ ├── saia_python/ # Main package
129
+ │ ├── __init__.py # Public API, version, functional wrappers
130
+ │ ├── client.py # SAIAClient — composes all services
131
+ │ ├── chat.py # ChatService — completions + streaming
132
+ │ ├── voice.py # VoiceService — transcribe + translate
133
+ │ ├── arcana.py # ArcanaService — RAG / knowledge bases
134
+ │ ├── models.py # ModelsService — list available models
135
+ │ ├── documents.py # DocumentService — Docling conversion
136
+ │ ├── openai_compat.py # OpenAI SDK compatibility layer
137
+ │ ├── auth.py # Credential and config discovery
138
+ │ ├── rate_limits.py # RateLimitInfo dataclass + parser
139
+ │ ├── exceptions.py # SAIAError hierarchy + raise_for_status
140
+ │ ├── _streaming.py # Shared SSE iterator
141
+ │ └── py.typed # PEP 561 typing marker
142
+ ├── docs/ # Sphinx documentation (PyData theme)
143
+ │ ├── conf.py
144
+ │ ├── index.rst
145
+ │ ├── quickstart.rst
146
+ │ ├── explanations.rst
147
+ │ ├── architecture.rst
148
+ │ ├── implementation.rst
149
+ │ ├── configuration.rst
150
+ │ ├── api/ # API reference (one page per module)
151
+ │ ├── development.rst
152
+ │ ├── dev_notes.rst
153
+ │ ├── testing.rst
154
+ │ ├── roadmap.rst
155
+ │ └── CHANGELOG.md
156
+ ├── tests/ # Unit tests
157
+ ├── examples/
158
+ │ ├── saia_python_demo.ipynb # Interactive demo
159
+ │ ├── openai_compatible_proxy.ipynb # OpenAI-compatible proxy example
160
+ │ ├── config.toml.example # Template for structured config
161
+ │ └── .env.example # Template for secrets (.env)
162
+ ├── .github/workflows/ # CI/CD (tests, docs, PyPI publish)
163
+ ├── pyproject.toml # Package metadata + dependencies
164
+ ├── CITATION.cff # Citation metadata (CFF 1.2.0)
165
+ ├── .gitignore
166
+ └── README.md
167
+ ```
168
+
169
+ ## Documentation
170
+
171
+ Online documentation: <https://fschwar4.github.io/saia_python/>
172
+
173
+ Build the docs locally:
174
+
175
+ ```bash
176
+ pip install -e ".[docs]"
177
+ sphinx-build -b html -w warnings_sphinx_build.txt docs docs/_build/html
178
+ python3 -m http.server 8000 --directory docs/_build/html
179
+ ```
180
+
181
+ ## Citation
182
+
183
+ If you use `saia-python` in your work, please cite it. Citation metadata lives in
184
+ [`CITATION.cff`](https://github.com/fschwar4/saia_python/blob/main/CITATION.cff); GitHub's "Cite this repository" button renders it
185
+ as APA or BibTeX. A Zenodo DOI will be added here once the first release is
186
+ archived.
187
+
188
+ ## License
189
+
190
+ [AGPL-3.0-only](https://github.com/fschwar4/saia_python/blob/main/LICENSE)
@@ -0,0 +1,22 @@
1
+ saia_python/__init__.py,sha256=GY3m01r5-kIwKGVQXwGbGAUWXLytDxsYt7c7yY5okyk,6377
2
+ saia_python/_http.py,sha256=6CNcpFP1u5ANzeDMb_QtYSwWe-fHG1no1nCNLSwzLz0,2793
3
+ saia_python/_streaming.py,sha256=e0LhJ-0yzrqC1Lzi_Rn0Xy8mBNGl6u2mAT1rMQGHqHc,2880
4
+ saia_python/_util.py,sha256=1QrgVxAEK6CwJTNK36qLv9GKTs3ySqg4FJKjx_ZEolM,867
5
+ saia_python/arcana.py,sha256=zYXVkTNggLCo_EtTiNm5W1Y8HIupl4CsrBK39EMHzuE,39732
6
+ saia_python/arcana_references.py,sha256=wVhbsQdqjtKaiEJu0Lpp26Nbqne7rXgHNE6qKH6cSfA,6818
7
+ saia_python/auth.py,sha256=iKKT7hI3oPtz_rq-wLj-S-_XT7RIGIFgeNrY5nCNNB4,16432
8
+ saia_python/chat.py,sha256=KsImp7mXCXAmzFtHRoxm8MMmn5X6zl-0_5xl5JYaOeo,2395
9
+ saia_python/client.py,sha256=yqjN1kv-nBQ81YaMNEtU07AbGQl6s-7aVQN-MwSO4wQ,8432
10
+ saia_python/documents.py,sha256=GG3DnfT6a4p5mPjxddqVr5yOAkceAH59j4UIkvTHrIs,4790
11
+ saia_python/exceptions.py,sha256=_eY-JSfubWvYkKV7GVbFrdHip7MfwdHwL6In-MbP9X8,2074
12
+ saia_python/models.py,sha256=XrMJ_ROs9k2SGAMg_goNpRh4CCpd0KrZP_7qOkNQtIs,4925
13
+ saia_python/openai_compat.py,sha256=RkZp-uKLpOk-UlTMpQpF_E-1_LtiAbUC4IqLWBOjM5k,2349
14
+ saia_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ saia_python/rate_limits.py,sha256=ITIF8CQkHpPVT1d_zEiFNXSt6uFQYagp9QE3C1i_m6Q,2841
16
+ saia_python/responses.py,sha256=alemqTlwpeVS3u7W_DG0HG-KCW19ficjpP5Cs6GvqR8,2206
17
+ saia_python/voice.py,sha256=hI365P1CLW3R2fwZrK2IHnr5bL4nXU570wfQ9Xd4fRQ,6313
18
+ saia_python-0.4.1.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
19
+ saia_python-0.4.1.dist-info/METADATA,sha256=t7dasMAaMqJxWTLraOBzjjxt4jo_R6tfSfEpYgSvIlU,8338
20
+ saia_python-0.4.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
+ saia_python-0.4.1.dist-info/top_level.txt,sha256=xQzdM-ir7Don4KJtPWWUkyLX9Ry2rai2OrnaWc7GMKw,12
22
+ saia_python-0.4.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+