saia-python 0.6.0__tar.gz → 0.8.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.
- {saia_python-0.6.0/saia_python.egg-info → saia_python-0.8.0}/PKG-INFO +8 -1
- {saia_python-0.6.0 → saia_python-0.8.0}/README.md +2 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/pyproject.toml +11 -1
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/__init__.py +45 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/arcana.py +107 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/client.py +15 -0
- saia_python-0.8.0/saia_python/tokenizer.py +1546 -0
- {saia_python-0.6.0 → saia_python-0.8.0/saia_python.egg-info}/PKG-INFO +8 -1
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python.egg-info/SOURCES.txt +2 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python.egg-info/requires.txt +6 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_arcana.py +85 -0
- saia_python-0.8.0/tests/test_tokenizer.py +656 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/LICENSE +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/_http.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/_streaming.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/_util.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/arcana_references.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/auth.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/chat.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/documents.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/exceptions.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/models.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/openai_compat.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/py.typed +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/rate_limits.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/responses.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python/voice.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python.egg-info/dependency_links.txt +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/saia_python.egg-info/top_level.txt +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/setup.cfg +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_arcana_references.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_auth.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_chat.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_client.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_documents.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_exceptions.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_health_check.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_models.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_openai_compat.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_rate_limits.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_responses.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_setup_from_directory.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_streaming.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_transport_policy.py +0 -0
- {saia_python-0.6.0 → saia_python-0.8.0}/tests/test_voice.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: saia-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Python wrapper for the GWDG SAIA platform REST API
|
|
5
5
|
Author: Friedrich Schwarz
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -31,6 +31,11 @@ Requires-Dist: tqdm>=4.60
|
|
|
31
31
|
Requires-Dist: tomlkit>=0.12
|
|
32
32
|
Provides-Extra: openai
|
|
33
33
|
Requires-Dist: openai>=1.0; extra == "openai"
|
|
34
|
+
Provides-Extra: tokenizer
|
|
35
|
+
Requires-Dist: transformers>=4.40; extra == "tokenizer"
|
|
36
|
+
Requires-Dist: huggingface-hub>=0.20; extra == "tokenizer"
|
|
37
|
+
Requires-Dist: tiktoken>=0.5; extra == "tokenizer"
|
|
38
|
+
Requires-Dist: sentencepiece>=0.1.99; extra == "tokenizer"
|
|
34
39
|
Provides-Extra: test
|
|
35
40
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
36
41
|
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
@@ -117,6 +122,7 @@ chat_completion(model="meta-llama-3.1-8b-instruct", messages=[...])
|
|
|
117
122
|
| **ARCANA** | RAG — knowledge base management and retrieval-augmented chat | [ARCANA](https://docs.hpc.gwdg.de/services/ai-services/arcana/index.html) |
|
|
118
123
|
| **Documents** | PDF/document conversion via Docling | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
|
|
119
124
|
| **Models** | List available models, probe tool-calling support | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
|
|
125
|
+
| **Tokenizers** | Download model tokenizers; count chat-template tokens, special-token overhead, and subword fertility (opt-in `[tokenizer]` extra) | [Chat AI Models](https://docs.hpc.gwdg.de/services/ai-services/chat-ai/models/index.html) |
|
|
120
126
|
| **Rate Limits** | Inspect current quota and usage | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
|
|
121
127
|
|
|
122
128
|
## Repository Structure
|
|
@@ -130,6 +136,7 @@ saia-python/
|
|
|
130
136
|
│ ├── voice.py # VoiceService — transcribe + translate
|
|
131
137
|
│ ├── arcana.py # ArcanaService — RAG / knowledge bases
|
|
132
138
|
│ ├── models.py # ModelsService — list available models
|
|
139
|
+
│ ├── tokenizer.py # Tokenizers — download, chat-template token counting
|
|
133
140
|
│ ├── documents.py # DocumentService — Docling conversion
|
|
134
141
|
│ ├── openai_compat.py # OpenAI SDK compatibility layer
|
|
135
142
|
│ ├── auth.py # Credential and config discovery
|
|
@@ -65,6 +65,7 @@ chat_completion(model="meta-llama-3.1-8b-instruct", messages=[...])
|
|
|
65
65
|
| **ARCANA** | RAG — knowledge base management and retrieval-augmented chat | [ARCANA](https://docs.hpc.gwdg.de/services/ai-services/arcana/index.html) |
|
|
66
66
|
| **Documents** | PDF/document conversion via Docling | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
|
|
67
67
|
| **Models** | List available models, probe tool-calling support | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
|
|
68
|
+
| **Tokenizers** | Download model tokenizers; count chat-template tokens, special-token overhead, and subword fertility (opt-in `[tokenizer]` extra) | [Chat AI Models](https://docs.hpc.gwdg.de/services/ai-services/chat-ai/models/index.html) |
|
|
68
69
|
| **Rate Limits** | Inspect current quota and usage | [SAIA API](https://docs.hpc.gwdg.de/services/ai-services/saia/index.html) |
|
|
69
70
|
|
|
70
71
|
## Repository Structure
|
|
@@ -78,6 +79,7 @@ saia-python/
|
|
|
78
79
|
│ ├── voice.py # VoiceService — transcribe + translate
|
|
79
80
|
│ ├── arcana.py # ArcanaService — RAG / knowledge bases
|
|
80
81
|
│ ├── models.py # ModelsService — list available models
|
|
82
|
+
│ ├── tokenizer.py # Tokenizers — download, chat-template token counting
|
|
81
83
|
│ ├── documents.py # DocumentService — Docling conversion
|
|
82
84
|
│ ├── openai_compat.py # OpenAI SDK compatibility layer
|
|
83
85
|
│ ├── auth.py # Credential and config discovery
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "saia-python"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Python wrapper for the GWDG SAIA platform REST API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -62,6 +62,16 @@ saia_python = ["py.typed"]
|
|
|
62
62
|
openai = [
|
|
63
63
|
"openai>=1.0",
|
|
64
64
|
]
|
|
65
|
+
tokenizer = [
|
|
66
|
+
# AutoTokenizer + chat-template (apply_chat_template) engine
|
|
67
|
+
"transformers>=4.40",
|
|
68
|
+
# downloads tokenizer files (snapshot_download) to the local cache
|
|
69
|
+
"huggingface-hub>=0.20",
|
|
70
|
+
# byte-pair encodings for the externally hosted OpenAI models
|
|
71
|
+
"tiktoken>=0.5",
|
|
72
|
+
# slow-tokenizer / SentencePiece backing for several of the models
|
|
73
|
+
"sentencepiece>=0.1.99",
|
|
74
|
+
]
|
|
65
75
|
test = [
|
|
66
76
|
"pytest>=7.0",
|
|
67
77
|
"pytest-cov>=4.0",
|
|
@@ -42,6 +42,29 @@ from .exceptions import APIError, AuthenticationError, RateLimitError, SAIAError
|
|
|
42
42
|
from .openai_compat import create_openai_client
|
|
43
43
|
from .rate_limits import RateLimitInfo, parse_rate_limits
|
|
44
44
|
from .responses import text_of
|
|
45
|
+
from .tokenizer import (
|
|
46
|
+
DEFAULT_TOKENIZER_DIR,
|
|
47
|
+
GWDG_MODEL_REPOS,
|
|
48
|
+
OPENAI_TIKTOKEN_ENCODINGS,
|
|
49
|
+
ChatTokenCount,
|
|
50
|
+
FileTokenCount,
|
|
51
|
+
GatedRepoAccessError,
|
|
52
|
+
TokenDistribution,
|
|
53
|
+
TokenizerService,
|
|
54
|
+
available_open_models,
|
|
55
|
+
chat_template_length,
|
|
56
|
+
chat_template_tokens,
|
|
57
|
+
count_tiktoken_tokens,
|
|
58
|
+
download_all_tokenizers,
|
|
59
|
+
download_tokenizer,
|
|
60
|
+
load_hf_token,
|
|
61
|
+
load_tokenizer,
|
|
62
|
+
repo_url,
|
|
63
|
+
resolve_repo,
|
|
64
|
+
special_token_overhead,
|
|
65
|
+
subword_fertility,
|
|
66
|
+
token_distribution,
|
|
67
|
+
)
|
|
45
68
|
|
|
46
69
|
try:
|
|
47
70
|
__version__ = version("saia-python")
|
|
@@ -81,6 +104,28 @@ __all__ = [
|
|
|
81
104
|
"parse_arcana_references",
|
|
82
105
|
"parse_reference_entries",
|
|
83
106
|
"is_arcana_event",
|
|
107
|
+
# Tokenizers ([tokenizer] extra)
|
|
108
|
+
"GWDG_MODEL_REPOS",
|
|
109
|
+
"OPENAI_TIKTOKEN_ENCODINGS",
|
|
110
|
+
"DEFAULT_TOKENIZER_DIR",
|
|
111
|
+
"ChatTokenCount",
|
|
112
|
+
"FileTokenCount",
|
|
113
|
+
"TokenDistribution",
|
|
114
|
+
"TokenizerService",
|
|
115
|
+
"GatedRepoAccessError",
|
|
116
|
+
"available_open_models",
|
|
117
|
+
"resolve_repo",
|
|
118
|
+
"repo_url",
|
|
119
|
+
"load_hf_token",
|
|
120
|
+
"download_tokenizer",
|
|
121
|
+
"download_all_tokenizers",
|
|
122
|
+
"load_tokenizer",
|
|
123
|
+
"chat_template_tokens",
|
|
124
|
+
"chat_template_length",
|
|
125
|
+
"special_token_overhead",
|
|
126
|
+
"subword_fertility",
|
|
127
|
+
"count_tiktoken_tokens",
|
|
128
|
+
"token_distribution",
|
|
84
129
|
# Functional API
|
|
85
130
|
"list_models",
|
|
86
131
|
"list_model_ids",
|
|
@@ -415,6 +415,49 @@ class ArcanaService:
|
|
|
415
415
|
|
|
416
416
|
return _json_or_none(resp)
|
|
417
417
|
|
|
418
|
+
def recreate(self, name: str, *, update_toml: bool = False) -> dict:
|
|
419
|
+
"""Delete an arcana and recreate it **empty with the same ID**.
|
|
420
|
+
|
|
421
|
+
The minimal-call way to wipe an entire arcana: two requests
|
|
422
|
+
(:meth:`delete` + :meth:`create`) regardless of how many files it
|
|
423
|
+
holds, versus one :meth:`delete_file` per file (thousands of calls,
|
|
424
|
+
each its own read-timeout risk while the arcana is busy). The name —
|
|
425
|
+
including any UUID suffix — is preserved verbatim via
|
|
426
|
+
``create(..., append_uuid=False)``, so a downstream pin on the full
|
|
427
|
+
``owner/name-uuid`` ID stays valid.
|
|
428
|
+
|
|
429
|
+
Trade-offs versus emptying file-by-file (:meth:`delete_file` in a
|
|
430
|
+
loop, which keeps the container): there is a brief window between the
|
|
431
|
+
two calls where the arcana does **not exist**, and the recreated
|
|
432
|
+
arcana is brand-new — ``created_at``, sharing/permissions and any
|
|
433
|
+
other container settings reset to defaults. Only the name/ID carries
|
|
434
|
+
across.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
name: The arcana name or full ``owner/name`` ID to recreate.
|
|
438
|
+
update_toml: Forwarded to :meth:`create`.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
The :meth:`create` result (``{"name", "id", "message"}``).
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
APIError: If recreation fails *after* the delete already
|
|
445
|
+
succeeded — the arcana is then gone. The message says so
|
|
446
|
+
explicitly so the operator recreates it before any consumer
|
|
447
|
+
(adapter / manifest pin) points at the ID.
|
|
448
|
+
"""
|
|
449
|
+
short = extract_arcana_name(name)
|
|
450
|
+
self.delete(name)
|
|
451
|
+
try:
|
|
452
|
+
return self.create(short, append_uuid=False, update_toml=update_toml)
|
|
453
|
+
except Exception as e: # noqa: BLE001 — re-raise with a louder message
|
|
454
|
+
raise APIError(
|
|
455
|
+
f"arcana {short!r} was DELETED but could not be recreated: {e}. "
|
|
456
|
+
f"The arcana no longer exists — recreate it (e.g. "
|
|
457
|
+
f"create({short!r}, append_uuid=False)) before any consumer "
|
|
458
|
+
f"(adapter / manifest pin) points at its ID."
|
|
459
|
+
) from e
|
|
460
|
+
|
|
418
461
|
def list(self) -> list[dict]:
|
|
419
462
|
"""List all available arcanas.
|
|
420
463
|
|
|
@@ -742,6 +785,70 @@ class ArcanaService:
|
|
|
742
785
|
raise_for_status(resp)
|
|
743
786
|
return _json_or_none(resp)
|
|
744
787
|
|
|
788
|
+
def delete_files(
|
|
789
|
+
self,
|
|
790
|
+
name: str,
|
|
791
|
+
file_names: Iterable[str],
|
|
792
|
+
*,
|
|
793
|
+
verbose: bool = False,
|
|
794
|
+
on_result: Callable[[str, dict], None] | None = None,
|
|
795
|
+
) -> list[dict]:
|
|
796
|
+
"""Delete an explicit list of files from an arcana, by name.
|
|
797
|
+
|
|
798
|
+
The batch counterpart to :meth:`delete_file`: hand it the file names
|
|
799
|
+
(as returned by :meth:`list_files`) and it deletes each one,
|
|
800
|
+
capturing a per-file outcome instead of aborting on the first
|
|
801
|
+
failure. Unlike :meth:`delete_directory` no local directory is
|
|
802
|
+
consulted — the names come straight from the caller — so it can
|
|
803
|
+
target files that no longer exist on disk (e.g. a name list from a
|
|
804
|
+
CSV). Pairs with a thin CLI front-end that resolves *which* names to
|
|
805
|
+
delete; this method only does the deleting.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
name: The arcana name or full ``owner/name`` ID.
|
|
809
|
+
file_names: The file names to delete (flat names, as listed by
|
|
810
|
+
:meth:`list_files`). Order is preserved; a repeated name is
|
|
811
|
+
attempted each time (a second delete just reports the
|
|
812
|
+
server's response / 404).
|
|
813
|
+
verbose: If ``True``, print per-file deletion status.
|
|
814
|
+
on_result: Optional callback invoked as ``on_result(file_name,
|
|
815
|
+
entry)`` after each file (``entry`` is that file's
|
|
816
|
+
``{"file", "status", ["error"]}`` dict), for inline
|
|
817
|
+
per-file logging.
|
|
818
|
+
|
|
819
|
+
Returns:
|
|
820
|
+
A list of dicts with keys ``"file"`` (the name), ``"status"``
|
|
821
|
+
(``"deleted"`` or ``"failed"``), and ``"error"`` (only on
|
|
822
|
+
failure) — the same shape every batch op returns.
|
|
823
|
+
"""
|
|
824
|
+
names = [str(n) for n in file_names]
|
|
825
|
+
# Arcana file names are flat (as listed by :meth:`list_files`). The
|
|
826
|
+
# batch executor below is Path-centric and targets ``Path(n).name``;
|
|
827
|
+
# a name containing ``/`` would silently collapse to its basename and
|
|
828
|
+
# delete the WRONG file. Refuse the whole batch up front — atomic, so
|
|
829
|
+
# nothing is deleted if any name is malformed (e.g. from a bad CSV).
|
|
830
|
+
bad = [n for n in names if "/" in n]
|
|
831
|
+
if bad:
|
|
832
|
+
raise ValueError(
|
|
833
|
+
f"ARCANA file names are flat (no '/'); refusing to delete "
|
|
834
|
+
f"{len(bad)} name(s) containing a path separator: {bad}"
|
|
835
|
+
)
|
|
836
|
+
# Reuse the shared batch executor (iteration, per-file error capture,
|
|
837
|
+
# progress bar, on_result, tally). With flat names ``Path(n).name ==
|
|
838
|
+
# n``, so the label/delete-target round-trips exactly.
|
|
839
|
+
return self._run_file_batch(
|
|
840
|
+
[Path(n) for n in names],
|
|
841
|
+
lambda fp: self.delete_file(name, fp.name),
|
|
842
|
+
default_status="deleted",
|
|
843
|
+
desc="Deleting",
|
|
844
|
+
verbose=verbose,
|
|
845
|
+
on_result=(
|
|
846
|
+
None
|
|
847
|
+
if on_result is None
|
|
848
|
+
else lambda fp, entry: on_result(fp.name, entry)
|
|
849
|
+
),
|
|
850
|
+
)
|
|
851
|
+
|
|
745
852
|
def download_file(self, name: str, file_name: str, output_path: str | Path) -> Path:
|
|
746
853
|
"""Download a file from an arcana to a local path.
|
|
747
854
|
|
|
@@ -12,6 +12,7 @@ from .documents import DocumentService
|
|
|
12
12
|
from .exceptions import raise_for_status
|
|
13
13
|
from .models import ModelsService
|
|
14
14
|
from .rate_limits import RateLimitInfo, parse_rate_limits
|
|
15
|
+
from .tokenizer import TokenizerService
|
|
15
16
|
from .voice import VoiceService
|
|
16
17
|
|
|
17
18
|
|
|
@@ -80,6 +81,7 @@ class SAIAClient:
|
|
|
80
81
|
self._models: ModelsService | None = None
|
|
81
82
|
self._arcana: ArcanaService | None = None
|
|
82
83
|
self._documents: DocumentService | None = None
|
|
84
|
+
self._tokenizers: TokenizerService | None = None
|
|
83
85
|
self._openai = None
|
|
84
86
|
self._openai_async = None
|
|
85
87
|
|
|
@@ -106,6 +108,19 @@ class SAIAClient:
|
|
|
106
108
|
)
|
|
107
109
|
return self._models
|
|
108
110
|
|
|
111
|
+
@property
|
|
112
|
+
def tokenizers(self) -> TokenizerService:
|
|
113
|
+
"""Tokenizer service for the open-weight models.
|
|
114
|
+
|
|
115
|
+
Loads model tokenizers, counts chat-template tokens, and annotates the
|
|
116
|
+
live model list with Hugging Face repositories. Requires the optional
|
|
117
|
+
``[tokenizer]`` extra (``pip install saia-python[tokenizer]``) for the
|
|
118
|
+
download/load operations; the repository annotations work without it.
|
|
119
|
+
"""
|
|
120
|
+
if self._tokenizers is None:
|
|
121
|
+
self._tokenizers = TokenizerService(self.models)
|
|
122
|
+
return self._tokenizers
|
|
123
|
+
|
|
109
124
|
@property
|
|
110
125
|
def arcana(self) -> ArcanaService:
|
|
111
126
|
"""ARCANA/RAG service."""
|