orca-sdk 0.1.9__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.
- orca_sdk/__init__.py +30 -0
- orca_sdk/_shared/__init__.py +10 -0
- orca_sdk/_shared/metrics.py +634 -0
- orca_sdk/_shared/metrics_test.py +570 -0
- orca_sdk/_utils/__init__.py +0 -0
- orca_sdk/_utils/analysis_ui.py +196 -0
- orca_sdk/_utils/analysis_ui_style.css +51 -0
- orca_sdk/_utils/auth.py +65 -0
- orca_sdk/_utils/auth_test.py +31 -0
- orca_sdk/_utils/common.py +37 -0
- orca_sdk/_utils/data_parsing.py +129 -0
- orca_sdk/_utils/data_parsing_test.py +244 -0
- orca_sdk/_utils/pagination.py +126 -0
- orca_sdk/_utils/pagination_test.py +132 -0
- orca_sdk/_utils/prediction_result_ui.css +18 -0
- orca_sdk/_utils/prediction_result_ui.py +110 -0
- orca_sdk/_utils/tqdm_file_reader.py +12 -0
- orca_sdk/_utils/value_parser.py +45 -0
- orca_sdk/_utils/value_parser_test.py +39 -0
- orca_sdk/async_client.py +4104 -0
- orca_sdk/classification_model.py +1165 -0
- orca_sdk/classification_model_test.py +887 -0
- orca_sdk/client.py +4096 -0
- orca_sdk/conftest.py +382 -0
- orca_sdk/credentials.py +217 -0
- orca_sdk/credentials_test.py +121 -0
- orca_sdk/datasource.py +576 -0
- orca_sdk/datasource_test.py +463 -0
- orca_sdk/embedding_model.py +712 -0
- orca_sdk/embedding_model_test.py +206 -0
- orca_sdk/job.py +343 -0
- orca_sdk/job_test.py +108 -0
- orca_sdk/memoryset.py +3811 -0
- orca_sdk/memoryset_test.py +1150 -0
- orca_sdk/regression_model.py +841 -0
- orca_sdk/regression_model_test.py +595 -0
- orca_sdk/telemetry.py +742 -0
- orca_sdk/telemetry_test.py +119 -0
- orca_sdk-0.1.9.dist-info/METADATA +98 -0
- orca_sdk-0.1.9.dist-info/RECORD +41 -0
- orca_sdk-0.1.9.dist-info/WHEEL +4 -0
orca_sdk/conftest.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Generator
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import pytest_asyncio
|
|
8
|
+
from datasets import ClassLabel, Dataset, Features, Value
|
|
9
|
+
|
|
10
|
+
from ._utils.auth import _create_api_key, _delete_org
|
|
11
|
+
from .async_client import OrcaAsyncClient
|
|
12
|
+
from .classification_model import ClassificationModel
|
|
13
|
+
from .client import OrcaClient
|
|
14
|
+
from .credentials import OrcaCredentials
|
|
15
|
+
from .datasource import Datasource
|
|
16
|
+
from .embedding_model import PretrainedEmbeddingModel
|
|
17
|
+
from .memoryset import LabeledMemoryset, ScoredMemoryset
|
|
18
|
+
from .regression_model import RegressionModel
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
21
|
+
|
|
22
|
+
os.environ["ORCA_API_URL"] = os.environ.get("ORCA_API_URL", "http://localhost:1584/")
|
|
23
|
+
|
|
24
|
+
os.environ["ORCA_SAVE_TELEMETRY_SYNCHRONOUSLY"] = "true"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def skip_in_prod(reason: str):
|
|
28
|
+
"""Custom decorator to skip tests when running against production API"""
|
|
29
|
+
PROD_API_URLs = ["https://api.orcadb.ai", "https://api.staging.orcadb.ai"]
|
|
30
|
+
return pytest.mark.skipif(
|
|
31
|
+
os.environ["ORCA_API_URL"] in PROD_API_URLs,
|
|
32
|
+
reason=reason,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def skip_in_ci(reason: str):
|
|
37
|
+
"""Custom decorator to skip tests when running in CI"""
|
|
38
|
+
return pytest.mark.skipif(
|
|
39
|
+
os.environ.get("GITHUB_ACTIONS", "false") == "true",
|
|
40
|
+
reason=reason,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _create_org_id():
|
|
45
|
+
# UUID start to identify test data (0xtest...)
|
|
46
|
+
return "10e50000-0000-4000-a000-" + str(uuid4())[24:]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture(scope="session")
|
|
50
|
+
def org_id():
|
|
51
|
+
return _create_org_id()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture(scope="session")
|
|
55
|
+
def other_org_id():
|
|
56
|
+
return _create_org_id()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture(autouse=True, scope="session")
|
|
60
|
+
def api_key(org_id) -> Generator[str, None, None]:
|
|
61
|
+
api_key = _create_api_key(org_id=org_id, name="orca_sdk_test")
|
|
62
|
+
with OrcaClient(api_key=api_key).use():
|
|
63
|
+
yield api_key
|
|
64
|
+
_delete_org(org_id)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# We cannot use a session scoped fixture because async pytest tears down the client after each test
|
|
68
|
+
@pytest.fixture(autouse=True)
|
|
69
|
+
def authenticate_async_client(api_key) -> Generator[None, None, None]:
|
|
70
|
+
with OrcaAsyncClient(api_key=api_key).use():
|
|
71
|
+
yield
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.fixture(scope="session")
|
|
75
|
+
def unauthenticated_client() -> OrcaClient:
|
|
76
|
+
return OrcaClient(api_key=str(uuid4()))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest_asyncio.fixture()
|
|
80
|
+
def unauthenticated_async_client() -> OrcaAsyncClient:
|
|
81
|
+
return OrcaAsyncClient(api_key=str(uuid4()))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.fixture(scope="session")
|
|
85
|
+
def unauthorized_client(other_org_id):
|
|
86
|
+
different_api_key = _create_api_key(org_id=other_org_id, name="orca_sdk_test_other_org")
|
|
87
|
+
return OrcaClient(api_key=different_api_key)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.fixture(scope="session")
|
|
91
|
+
def predict_only_client() -> OrcaClient:
|
|
92
|
+
predict_api_key = OrcaCredentials.create_api_key("orca_sdk_test_predict", scopes={"PREDICT"})
|
|
93
|
+
return OrcaClient(api_key=predict_api_key)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture(scope="session")
|
|
97
|
+
def label_names():
|
|
98
|
+
return ["soup", "cats"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
SAMPLE_DATA = [
|
|
102
|
+
{"value": "i love soup", "label": 0, "key": "g1", "score": 0.1, "source_id": "s1", "partition_id": "p1"},
|
|
103
|
+
{"value": "cats are cute", "label": 1, "key": "g1", "score": 0.9, "source_id": "s2", "partition_id": "p1"},
|
|
104
|
+
{"value": "soup is good", "label": 0, "key": "g1", "score": 0.1, "source_id": "s3", "partition_id": "p1"},
|
|
105
|
+
{"value": "i love cats", "label": 1, "key": "g1", "score": 0.9, "source_id": "s4", "partition_id": "p1"},
|
|
106
|
+
{"value": "everyone loves cats", "label": 1, "key": "g1", "score": 0.9, "source_id": "s5", "partition_id": "p1"},
|
|
107
|
+
{
|
|
108
|
+
"value": "soup is great for the winter",
|
|
109
|
+
"label": 0,
|
|
110
|
+
"key": "g1",
|
|
111
|
+
"score": 0.1,
|
|
112
|
+
"source_id": "s6",
|
|
113
|
+
"partition_id": "p1",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"value": "hot soup on a rainy day!",
|
|
117
|
+
"label": 0,
|
|
118
|
+
"key": "g1",
|
|
119
|
+
"score": 0.1,
|
|
120
|
+
"source_id": "s7",
|
|
121
|
+
"partition_id": "p1",
|
|
122
|
+
},
|
|
123
|
+
{"value": "cats sleep all day", "label": 1, "key": "g1", "score": 0.9, "source_id": "s8", "partition_id": "p1"},
|
|
124
|
+
{"value": "homemade soup recipes", "label": 0, "key": "g1", "score": 0.1, "source_id": "s9", "partition_id": "p2"},
|
|
125
|
+
{"value": "cats purr when happy", "label": 1, "key": "g2", "score": 0.9, "source_id": "s10", "partition_id": "p2"},
|
|
126
|
+
{
|
|
127
|
+
"value": "chicken noodle soup is classic",
|
|
128
|
+
"label": 0,
|
|
129
|
+
"key": "g1",
|
|
130
|
+
"score": 0.1,
|
|
131
|
+
"source_id": "s11",
|
|
132
|
+
"partition_id": "p2",
|
|
133
|
+
},
|
|
134
|
+
{"value": "kittens are baby cats", "label": 1, "key": "g2", "score": 0.9, "source_id": "s12", "partition_id": "p2"},
|
|
135
|
+
{
|
|
136
|
+
"value": "soup can be served cold too",
|
|
137
|
+
"label": 0,
|
|
138
|
+
"key": "g1",
|
|
139
|
+
"score": 0.1,
|
|
140
|
+
"source_id": "s13",
|
|
141
|
+
"partition_id": "p2",
|
|
142
|
+
},
|
|
143
|
+
{"value": "cats have nine lives", "label": 1, "key": "g2", "score": 0.9, "source_id": "s14", "partition_id": "p2"},
|
|
144
|
+
{
|
|
145
|
+
"value": "tomato soup with grilled cheese",
|
|
146
|
+
"label": 0,
|
|
147
|
+
"key": "g1",
|
|
148
|
+
"score": 0.1,
|
|
149
|
+
"source_id": "s15",
|
|
150
|
+
"partition_id": "p2",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"value": "cats are independent animals",
|
|
154
|
+
"label": 1,
|
|
155
|
+
"key": "g2",
|
|
156
|
+
"score": 0.9,
|
|
157
|
+
"source_id": "s16",
|
|
158
|
+
"partition_id": None,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"value": "the beach is always fun",
|
|
162
|
+
"label": None,
|
|
163
|
+
"key": "g3",
|
|
164
|
+
"score": None,
|
|
165
|
+
"source_id": "s17",
|
|
166
|
+
"partition_id": None,
|
|
167
|
+
},
|
|
168
|
+
{"value": "i love the beach", "label": None, "key": "g3", "score": None, "source_id": "s18", "partition_id": None},
|
|
169
|
+
{
|
|
170
|
+
"value": "the ocean is healing",
|
|
171
|
+
"label": None,
|
|
172
|
+
"key": "g3",
|
|
173
|
+
"score": None,
|
|
174
|
+
"source_id": "s19",
|
|
175
|
+
"partition_id": None,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"value": "sandy feet, sand between my toes at the beach",
|
|
179
|
+
"label": None,
|
|
180
|
+
"key": "g3",
|
|
181
|
+
"score": None,
|
|
182
|
+
"source_id": "s20",
|
|
183
|
+
"partition_id": None,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"value": "i am such a beach bum",
|
|
187
|
+
"label": None,
|
|
188
|
+
"key": "g3",
|
|
189
|
+
"score": None,
|
|
190
|
+
"source_id": "s21",
|
|
191
|
+
"partition_id": None,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"value": "i will always want to be at the beach",
|
|
195
|
+
"label": None,
|
|
196
|
+
"key": "g3",
|
|
197
|
+
"score": None,
|
|
198
|
+
"source_id": "s22",
|
|
199
|
+
"partition_id": None,
|
|
200
|
+
},
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@pytest.fixture(scope="session")
|
|
205
|
+
def hf_dataset(label_names: list[str]) -> Dataset:
|
|
206
|
+
return Dataset.from_list(
|
|
207
|
+
SAMPLE_DATA,
|
|
208
|
+
features=Features(
|
|
209
|
+
{
|
|
210
|
+
"value": Value("string"),
|
|
211
|
+
"label": ClassLabel(names=label_names),
|
|
212
|
+
"key": Value("string"),
|
|
213
|
+
"score": Value("float"),
|
|
214
|
+
"source_id": Value("string"),
|
|
215
|
+
"partition_id": Value("string"),
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.fixture(scope="session")
|
|
222
|
+
def datasource(hf_dataset: Dataset) -> Datasource:
|
|
223
|
+
datasource = Datasource.from_hf_dataset("test_datasource", hf_dataset)
|
|
224
|
+
return datasource
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
EVAL_DATASET = [
|
|
228
|
+
{"value": "chicken noodle soup is the best", "label": 1, "score": 0.9}, # mislabeled
|
|
229
|
+
{"value": "cats are cute", "label": 0, "score": 0.1}, # mislabeled
|
|
230
|
+
{"value": "soup is great for the winter", "label": 0, "score": 0.1},
|
|
231
|
+
{"value": "i love cats", "label": 1, "score": 0.9},
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@pytest.fixture(scope="session")
|
|
236
|
+
def eval_datasource() -> Datasource:
|
|
237
|
+
eval_datasource = Datasource.from_list("eval_datasource", EVAL_DATASET)
|
|
238
|
+
return eval_datasource
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.fixture(scope="session")
|
|
242
|
+
def eval_dataset() -> Dataset:
|
|
243
|
+
eval_dataset = Dataset.from_list(EVAL_DATASET)
|
|
244
|
+
return eval_dataset
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@pytest.fixture(scope="session")
|
|
248
|
+
def readonly_memoryset(datasource: Datasource) -> LabeledMemoryset:
|
|
249
|
+
memoryset = LabeledMemoryset.create(
|
|
250
|
+
"test_readonly_memoryset",
|
|
251
|
+
datasource=datasource,
|
|
252
|
+
embedding_model=PretrainedEmbeddingModel.GTE_BASE,
|
|
253
|
+
source_id_column="source_id",
|
|
254
|
+
max_seq_length_override=32,
|
|
255
|
+
index_type="IVF_FLAT",
|
|
256
|
+
index_params={"n_lists": 100},
|
|
257
|
+
)
|
|
258
|
+
return memoryset
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@pytest.fixture(scope="session")
|
|
262
|
+
def readonly_partitioned_memoryset(datasource: Datasource) -> LabeledMemoryset:
|
|
263
|
+
memoryset = LabeledMemoryset.create(
|
|
264
|
+
"test_readonly_partitioned_memoryset",
|
|
265
|
+
datasource=datasource,
|
|
266
|
+
embedding_model=PretrainedEmbeddingModel.GTE_BASE,
|
|
267
|
+
source_id_column="source_id",
|
|
268
|
+
partition_id_column="partition_id",
|
|
269
|
+
)
|
|
270
|
+
return memoryset
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@pytest.fixture(scope="function")
|
|
274
|
+
def writable_memoryset(datasource: Datasource, api_key: str) -> Generator[LabeledMemoryset, None, None]:
|
|
275
|
+
"""
|
|
276
|
+
Function-scoped fixture that provides a writable memoryset for tests that mutate state.
|
|
277
|
+
|
|
278
|
+
This fixture creates a fresh `LabeledMemoryset` named 'test_writable_memoryset' before each test.
|
|
279
|
+
After the test, it attempts to restore the memoryset to its initial state by deleting any added entries
|
|
280
|
+
and reinserting sample data — unless the memoryset has been dropped by the test itself, in which case
|
|
281
|
+
it will be recreated on the next invocation.
|
|
282
|
+
|
|
283
|
+
Note: Re-creating the memoryset from scratch is surprisingly more expensive than cleaning it up.
|
|
284
|
+
"""
|
|
285
|
+
# It shouldn't be possible for this memoryset to already exist
|
|
286
|
+
memoryset = LabeledMemoryset.create(
|
|
287
|
+
"test_writable_memoryset",
|
|
288
|
+
datasource=datasource,
|
|
289
|
+
embedding_model=PretrainedEmbeddingModel.GTE_BASE,
|
|
290
|
+
source_id_column="source_id",
|
|
291
|
+
max_seq_length_override=32,
|
|
292
|
+
if_exists="open",
|
|
293
|
+
)
|
|
294
|
+
try:
|
|
295
|
+
yield memoryset
|
|
296
|
+
finally:
|
|
297
|
+
# Restore the memoryset to a clean state for the next test.
|
|
298
|
+
with OrcaClient(api_key=api_key).use():
|
|
299
|
+
if LabeledMemoryset.exists("test_writable_memoryset"):
|
|
300
|
+
memoryset.refresh()
|
|
301
|
+
|
|
302
|
+
memory_ids = [memoryset[i].memory_id for i in range(len(memoryset))]
|
|
303
|
+
|
|
304
|
+
if memory_ids:
|
|
305
|
+
memoryset.delete(memory_ids)
|
|
306
|
+
memoryset.refresh()
|
|
307
|
+
assert len(memoryset) == 0
|
|
308
|
+
memoryset.insert(SAMPLE_DATA)
|
|
309
|
+
# If the test dropped the memoryset, do nothing — it will be recreated on the next use.
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@pytest.fixture(scope="session")
|
|
313
|
+
def classification_model(readonly_memoryset: LabeledMemoryset) -> ClassificationModel:
|
|
314
|
+
model = ClassificationModel.create(
|
|
315
|
+
"test_classification_model",
|
|
316
|
+
readonly_memoryset,
|
|
317
|
+
num_classes=2,
|
|
318
|
+
memory_lookup_count=3,
|
|
319
|
+
description="test_description",
|
|
320
|
+
)
|
|
321
|
+
return model
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@pytest.fixture(scope="session")
|
|
325
|
+
def partitioned_classification_model(readonly_partitioned_memoryset: LabeledMemoryset) -> ClassificationModel:
|
|
326
|
+
model = ClassificationModel.create(
|
|
327
|
+
"test_partitioned_classification_model",
|
|
328
|
+
readonly_partitioned_memoryset,
|
|
329
|
+
num_classes=2,
|
|
330
|
+
memory_lookup_count=3,
|
|
331
|
+
description="test_partitioned_description",
|
|
332
|
+
)
|
|
333
|
+
return model
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# Add scored memoryset and regression model fixtures
|
|
337
|
+
@pytest.fixture(scope="session")
|
|
338
|
+
def scored_memoryset(datasource: Datasource) -> ScoredMemoryset:
|
|
339
|
+
memoryset = ScoredMemoryset.create(
|
|
340
|
+
"test_scored_memoryset",
|
|
341
|
+
datasource=datasource,
|
|
342
|
+
embedding_model=PretrainedEmbeddingModel.GTE_BASE,
|
|
343
|
+
source_id_column="source_id",
|
|
344
|
+
max_seq_length_override=32,
|
|
345
|
+
index_type="IVF_FLAT",
|
|
346
|
+
index_params={"n_lists": 100},
|
|
347
|
+
)
|
|
348
|
+
return memoryset
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@pytest.fixture(scope="session")
|
|
352
|
+
def regression_model(scored_memoryset: ScoredMemoryset) -> RegressionModel:
|
|
353
|
+
model = RegressionModel.create(
|
|
354
|
+
"test_regression_model",
|
|
355
|
+
scored_memoryset,
|
|
356
|
+
memory_lookup_count=3,
|
|
357
|
+
description="test_regression_description",
|
|
358
|
+
)
|
|
359
|
+
return model
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@pytest.fixture(scope="session")
|
|
363
|
+
def readonly_partitioned_scored_memoryset(datasource: Datasource) -> ScoredMemoryset:
|
|
364
|
+
memoryset = ScoredMemoryset.create(
|
|
365
|
+
"test_readonly_partitioned_scored_memoryset",
|
|
366
|
+
datasource=datasource,
|
|
367
|
+
embedding_model=PretrainedEmbeddingModel.GTE_BASE,
|
|
368
|
+
source_id_column="source_id",
|
|
369
|
+
partition_id_column="partition_id",
|
|
370
|
+
)
|
|
371
|
+
return memoryset
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@pytest.fixture(scope="session")
|
|
375
|
+
def partitioned_regression_model(readonly_partitioned_scored_memoryset: ScoredMemoryset) -> RegressionModel:
|
|
376
|
+
model = RegressionModel.create(
|
|
377
|
+
"test_partitioned_regression_model",
|
|
378
|
+
readonly_partitioned_scored_memoryset,
|
|
379
|
+
memory_lookup_count=3,
|
|
380
|
+
description="test_partitioned_regression_description",
|
|
381
|
+
)
|
|
382
|
+
return model
|
orca_sdk/credentials.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Literal, NamedTuple
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from httpx import ConnectError, Headers, HTTPTransport
|
|
7
|
+
from typing_extensions import deprecated
|
|
8
|
+
|
|
9
|
+
from .async_client import OrcaAsyncClient
|
|
10
|
+
from .client import OrcaClient
|
|
11
|
+
|
|
12
|
+
Scope = Literal["ADMINISTER", "PREDICT"]
|
|
13
|
+
"""
|
|
14
|
+
The scopes of an API key.
|
|
15
|
+
|
|
16
|
+
- `ADMINISTER`: Can do anything, including creating and deleting organizations, models, and API keys.
|
|
17
|
+
- `PREDICT`: Can only call model.predict and perform CRUD operations on predictions.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApiKeyInfo:
|
|
22
|
+
"""
|
|
23
|
+
Information about an API key
|
|
24
|
+
|
|
25
|
+
Note:
|
|
26
|
+
The value of the API key is only available at creation time.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
name: Unique name of the API key
|
|
30
|
+
created_at: When the API key was created
|
|
31
|
+
scopes: The scopes of the API key
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
created_at: datetime
|
|
36
|
+
scopes: set[Scope]
|
|
37
|
+
|
|
38
|
+
def __init__(self, name: str, created_at: datetime, scopes: set[Scope]):
|
|
39
|
+
self.name = name
|
|
40
|
+
self.created_at = created_at
|
|
41
|
+
self.scopes = scopes
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return "ApiKey({ " + f"name: '{self.name}', scopes: <{'|'.join(self.scopes)}>" + "})"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OrcaCredentials:
|
|
48
|
+
"""
|
|
49
|
+
Class for managing Orca API credentials
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def is_authenticated() -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Check if you are authenticated to interact with the Orca API
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if you are authenticated, False otherwise
|
|
59
|
+
"""
|
|
60
|
+
client = OrcaClient._resolve_client()
|
|
61
|
+
try:
|
|
62
|
+
return client.GET("/auth")
|
|
63
|
+
except ValueError as e:
|
|
64
|
+
if "Invalid API key" in str(e):
|
|
65
|
+
return False
|
|
66
|
+
raise e
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def is_healthy() -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Check whether the API is healthy
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if the API is healthy, False otherwise
|
|
75
|
+
"""
|
|
76
|
+
client = OrcaClient._resolve_client()
|
|
77
|
+
try:
|
|
78
|
+
# we don't want a retry transport here, so we use httpx directly
|
|
79
|
+
httpx.get(f"{client.base_url}/check/healthy")
|
|
80
|
+
except Exception:
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def list_api_keys() -> list[ApiKeyInfo]:
|
|
86
|
+
"""
|
|
87
|
+
List all API keys that have been created for your org
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A list of named tuples, with the name and creation date time of the API key
|
|
91
|
+
"""
|
|
92
|
+
client = OrcaClient._resolve_client()
|
|
93
|
+
return [
|
|
94
|
+
ApiKeyInfo(
|
|
95
|
+
name=api_key["name"],
|
|
96
|
+
created_at=datetime.fromisoformat(api_key["created_at"]),
|
|
97
|
+
scopes=set(api_key["scope"]),
|
|
98
|
+
)
|
|
99
|
+
for api_key in client.GET("/auth/api_key")
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def create_api_key(name: str, scopes: set[Scope] = {"ADMINISTER"}) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Create a new API key with the given name and scopes
|
|
106
|
+
|
|
107
|
+
Params:
|
|
108
|
+
name: The name of the API key
|
|
109
|
+
scopes: The scopes of the API key
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The secret value of the API key. Make sure to save this value as it will not be shown again.
|
|
113
|
+
"""
|
|
114
|
+
client = OrcaClient._resolve_client()
|
|
115
|
+
res = client.POST(
|
|
116
|
+
"/auth/api_key",
|
|
117
|
+
json={"name": name, "scope": list(scopes)},
|
|
118
|
+
)
|
|
119
|
+
return res["api_key"]
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def revoke_api_key(name: str) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Delete an API key
|
|
125
|
+
|
|
126
|
+
Params:
|
|
127
|
+
name: The name of the API key to delete
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: if the API key is not found
|
|
131
|
+
"""
|
|
132
|
+
client = OrcaClient._resolve_client()
|
|
133
|
+
client.DELETE("/auth/api_key/{name_or_id}", params={"name_or_id": name})
|
|
134
|
+
|
|
135
|
+
# TODO: remove deprecated methods after 2026-01-01
|
|
136
|
+
|
|
137
|
+
@deprecated("Use `OrcaClient.api_key` instead")
|
|
138
|
+
@staticmethod
|
|
139
|
+
def set_api_key(api_key: str, check_validity: bool = True):
|
|
140
|
+
"""
|
|
141
|
+
Set the API key to use for authenticating with the Orca API
|
|
142
|
+
|
|
143
|
+
Note:
|
|
144
|
+
The API key can also be provided by setting the `ORCA_API_KEY` environment variable
|
|
145
|
+
|
|
146
|
+
Params:
|
|
147
|
+
api_key: The API key to set
|
|
148
|
+
check_validity: Whether to check if the API key is valid and raise an error otherwise
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
ValueError: if the API key is invalid and `check_validity` is True
|
|
152
|
+
"""
|
|
153
|
+
sync_client = OrcaClient._resolve_client()
|
|
154
|
+
sync_client.api_key = api_key
|
|
155
|
+
if check_validity:
|
|
156
|
+
sync_client.GET("/auth")
|
|
157
|
+
|
|
158
|
+
async_client = OrcaAsyncClient._resolve_client()
|
|
159
|
+
async_client.api_key = api_key
|
|
160
|
+
|
|
161
|
+
@deprecated("Use `OrcaClient.base_url` instead")
|
|
162
|
+
@staticmethod
|
|
163
|
+
def get_api_url() -> str:
|
|
164
|
+
"""
|
|
165
|
+
Get the base URL of the Orca API that is currently being used
|
|
166
|
+
"""
|
|
167
|
+
client = OrcaClient._resolve_client()
|
|
168
|
+
return str(client.base_url)
|
|
169
|
+
|
|
170
|
+
@deprecated("Use `OrcaClient.base_url` instead")
|
|
171
|
+
@staticmethod
|
|
172
|
+
def set_api_url(url: str, check_validity: bool = True):
|
|
173
|
+
"""
|
|
174
|
+
Set the base URL for the Orca API
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
url: The base URL to set
|
|
178
|
+
check_validity: Whether to check if there is an API running at the given base URL
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
ValueError: if there is no healthy API running at the given base URL and `check_validity` is True
|
|
182
|
+
"""
|
|
183
|
+
# check if the base url is reachable before setting it
|
|
184
|
+
if check_validity:
|
|
185
|
+
try:
|
|
186
|
+
httpx.get(url, timeout=1)
|
|
187
|
+
except ConnectError as e:
|
|
188
|
+
raise ValueError(f"No API found at {url}") from e
|
|
189
|
+
|
|
190
|
+
sync_client = OrcaClient._resolve_client()
|
|
191
|
+
sync_client.base_url = url
|
|
192
|
+
|
|
193
|
+
async_client = OrcaAsyncClient._resolve_client()
|
|
194
|
+
async_client.base_url = url
|
|
195
|
+
|
|
196
|
+
# check if the api passes the health check
|
|
197
|
+
if check_validity:
|
|
198
|
+
OrcaCredentials.is_healthy()
|
|
199
|
+
|
|
200
|
+
@deprecated("Use `OrcaClient.headers` instead")
|
|
201
|
+
@staticmethod
|
|
202
|
+
def set_api_headers(headers: dict[str, str]):
|
|
203
|
+
"""
|
|
204
|
+
Add or override default HTTP headers for all Orca API requests.
|
|
205
|
+
|
|
206
|
+
Params:
|
|
207
|
+
headers: Mapping of header names to their string values
|
|
208
|
+
|
|
209
|
+
Notes:
|
|
210
|
+
New keys are merged into the existing headers, this will overwrite headers with the
|
|
211
|
+
same name, but leave other headers untouched.
|
|
212
|
+
"""
|
|
213
|
+
sync_client = OrcaClient._resolve_client()
|
|
214
|
+
sync_client.headers.update(Headers(headers))
|
|
215
|
+
|
|
216
|
+
async_client = OrcaAsyncClient._resolve_client()
|
|
217
|
+
async_client.headers.update(Headers(headers))
|