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.
Files changed (41) hide show
  1. orca_sdk/__init__.py +30 -0
  2. orca_sdk/_shared/__init__.py +10 -0
  3. orca_sdk/_shared/metrics.py +634 -0
  4. orca_sdk/_shared/metrics_test.py +570 -0
  5. orca_sdk/_utils/__init__.py +0 -0
  6. orca_sdk/_utils/analysis_ui.py +196 -0
  7. orca_sdk/_utils/analysis_ui_style.css +51 -0
  8. orca_sdk/_utils/auth.py +65 -0
  9. orca_sdk/_utils/auth_test.py +31 -0
  10. orca_sdk/_utils/common.py +37 -0
  11. orca_sdk/_utils/data_parsing.py +129 -0
  12. orca_sdk/_utils/data_parsing_test.py +244 -0
  13. orca_sdk/_utils/pagination.py +126 -0
  14. orca_sdk/_utils/pagination_test.py +132 -0
  15. orca_sdk/_utils/prediction_result_ui.css +18 -0
  16. orca_sdk/_utils/prediction_result_ui.py +110 -0
  17. orca_sdk/_utils/tqdm_file_reader.py +12 -0
  18. orca_sdk/_utils/value_parser.py +45 -0
  19. orca_sdk/_utils/value_parser_test.py +39 -0
  20. orca_sdk/async_client.py +4104 -0
  21. orca_sdk/classification_model.py +1165 -0
  22. orca_sdk/classification_model_test.py +887 -0
  23. orca_sdk/client.py +4096 -0
  24. orca_sdk/conftest.py +382 -0
  25. orca_sdk/credentials.py +217 -0
  26. orca_sdk/credentials_test.py +121 -0
  27. orca_sdk/datasource.py +576 -0
  28. orca_sdk/datasource_test.py +463 -0
  29. orca_sdk/embedding_model.py +712 -0
  30. orca_sdk/embedding_model_test.py +206 -0
  31. orca_sdk/job.py +343 -0
  32. orca_sdk/job_test.py +108 -0
  33. orca_sdk/memoryset.py +3811 -0
  34. orca_sdk/memoryset_test.py +1150 -0
  35. orca_sdk/regression_model.py +841 -0
  36. orca_sdk/regression_model_test.py +595 -0
  37. orca_sdk/telemetry.py +742 -0
  38. orca_sdk/telemetry_test.py +119 -0
  39. orca_sdk-0.1.9.dist-info/METADATA +98 -0
  40. orca_sdk-0.1.9.dist-info/RECORD +41 -0
  41. 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
@@ -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))