octostar-python-client 0.1.759__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.
- octostar/__init__.py +9 -0
- octostar/api/__init__.py +1 -0
- octostar/api/apps/__init__.py +0 -0
- octostar/api/apps/deploy_app.py +210 -0
- octostar/api/apps/execute_app_job.py +188 -0
- octostar/api/apps/get_app_logs.py +210 -0
- octostar/api/apps/get_apps_url.py +188 -0
- octostar/api/apps/get_job_logs.py +210 -0
- octostar/api/apps/get_job_progress.py +162 -0
- octostar/api/apps/kill_job.py +160 -0
- octostar/api/apps/list_app_jobs.py +276 -0
- octostar/api/apps/list_apps.py +251 -0
- octostar/api/apps/set_job_progress.py +216 -0
- octostar/api/apps/undeploy_app.py +160 -0
- octostar/api/metadata/__init__.py +0 -0
- octostar/api/metadata/get_version.py +232 -0
- octostar/api/metadata/get_whoami.py +232 -0
- octostar/api/notifications/__init__.py +0 -0
- octostar/api/notifications/delete_stream.py +222 -0
- octostar/api/notifications/get_subscriptions.py +240 -0
- octostar/api/notifications/publish_notification.py +275 -0
- octostar/api/notifications/pull_events_from_stream.py +282 -0
- octostar/api/notifications/push_event_to_stream.py +265 -0
- octostar/api/notifications/toast.py +264 -0
- octostar/api/ontology/__init__.py +0 -0
- octostar/api/ontology/fetch_ontology_data.py +275 -0
- octostar/api/ontology/get_ontologies.py +237 -0
- octostar/api/ontology/multi_query.py +297 -0
- octostar/api/ontology/query.py +276 -0
- octostar/api/pipeline/__init__.py +1 -0
- octostar/api/pipeline/get_processing_status.py +185 -0
- octostar/api/pipeline/update_processing_status.py +164 -0
- octostar/api/search/__init__.py +0 -0
- octostar/api/search/get_annotations.py +153 -0
- octostar/api/workspace_data/__init__.py +0 -0
- octostar/api/workspace_data/delete_blob.py +212 -0
- octostar/api/workspace_data/delete_entities.py +326 -0
- octostar/api/workspace_data/download_blob.py +235 -0
- octostar/api/workspace_data/get_attachment.py +336 -0
- octostar/api/workspace_data/get_files_tree.py +397 -0
- octostar/api/workspace_data/upload_blob.py +235 -0
- octostar/api/workspace_data/upsert_entities.py +284 -0
- octostar/api/workspace_permissions/__init__.py +0 -0
- octostar/api/workspace_permissions/get_permissions.py +325 -0
- octostar/api/workspace_tags/__init__.py +0 -0
- octostar/api/workspace_tags/delete_tag_from_entities.py +141 -0
- octostar/api/workspace_tags/tag_entities.py +180 -0
- octostar/client.py +492 -0
- octostar/errors.py +50 -0
- octostar/models/__init__.py +249 -0
- octostar/models/acknowledgement.py +74 -0
- octostar/models/acknowledgement_with_data.py +82 -0
- octostar/models/app_status.py +239 -0
- octostar/models/app_status_annotations.py +66 -0
- octostar/models/app_status_labels.py +69 -0
- octostar/models/app_with_url.py +82 -0
- octostar/models/child_processing_status.py +118 -0
- octostar/models/delete_entities_response_401.py +74 -0
- octostar/models/delete_entities_response_409.py +82 -0
- octostar/models/delete_entities_response_500.py +82 -0
- octostar/models/delete_stream_response_401.py +74 -0
- octostar/models/delete_tag_from_entities_response_401.py +74 -0
- octostar/models/deploy_app_json_body.py +90 -0
- octostar/models/deploy_app_json_body_secrets.py +65 -0
- octostar/models/deploy_app_response_200.py +98 -0
- octostar/models/deploy_app_response_200_data.py +60 -0
- octostar/models/deploy_app_response_400.py +82 -0
- octostar/models/deploy_app_response_403.py +82 -0
- octostar/models/deploy_app_response_404.py +82 -0
- octostar/models/deploy_app_response_409.py +82 -0
- octostar/models/deploy_app_response_500.py +82 -0
- octostar/models/entity.py +80 -0
- octostar/models/entity_response.py +99 -0
- octostar/models/entity_response_s3_urls.py +93 -0
- octostar/models/entity_response_s3_urls_additional_property.py +105 -0
- octostar/models/entity_response_s3_urls_additional_property_fields.py +114 -0
- octostar/models/execute_app_job_json_body.py +151 -0
- octostar/models/execute_app_job_json_body_annotation.py +65 -0
- octostar/models/execute_app_job_response_401.py +74 -0
- octostar/models/fetch_ontology_data_response_200.py +60 -0
- octostar/models/fetch_ontology_data_response_401.py +74 -0
- octostar/models/fetch_ontology_data_response_500.py +82 -0
- octostar/models/get_app_logs_response_401.py +74 -0
- octostar/models/get_app_logs_response_404.py +74 -0
- octostar/models/get_app_logs_response_500.py +82 -0
- octostar/models/get_apps_url_json_body.py +76 -0
- octostar/models/get_apps_url_response_401.py +74 -0
- octostar/models/get_apps_url_response_500.py +82 -0
- octostar/models/get_attachment_response_200.py +74 -0
- octostar/models/get_attachment_response_401.py +74 -0
- octostar/models/get_files_tree_response_200.py +106 -0
- octostar/models/get_files_tree_response_200_status.py +8 -0
- octostar/models/get_files_tree_response_400.py +111 -0
- octostar/models/get_files_tree_response_400_data.py +60 -0
- octostar/models/get_files_tree_response_400_status.py +8 -0
- octostar/models/get_files_tree_response_401.py +74 -0
- octostar/models/get_files_tree_response_500.py +111 -0
- octostar/models/get_files_tree_response_500_data.py +60 -0
- octostar/models/get_files_tree_response_500_status.py +8 -0
- octostar/models/get_job_logs_response_401.py +74 -0
- octostar/models/get_job_logs_response_404.py +74 -0
- octostar/models/get_job_logs_response_500.py +82 -0
- octostar/models/get_job_progress_response_401.py +74 -0
- octostar/models/get_object_response_401.py +74 -0
- octostar/models/get_ontologies_response_401.py +74 -0
- octostar/models/get_ontologies_response_500.py +81 -0
- octostar/models/get_permissions_response_200.py +98 -0
- octostar/models/get_permissions_response_400.py +82 -0
- octostar/models/get_permissions_response_401.py +74 -0
- octostar/models/get_permissions_response_500.py +82 -0
- octostar/models/get_processing_status_response_200.py +104 -0
- octostar/models/get_processing_status_response_200_data.py +87 -0
- octostar/models/get_processing_status_response_400.py +82 -0
- octostar/models/get_processing_status_response_500.py +82 -0
- octostar/models/get_subscriptions_response_200_item.py +74 -0
- octostar/models/get_version_response_200.py +74 -0
- octostar/models/get_version_response_404.py +74 -0
- octostar/models/get_whoami_response_200.py +129 -0
- octostar/models/get_whoami_response_401.py +74 -0
- octostar/models/insert_entity.py +114 -0
- octostar/models/insert_entity_base.py +266 -0
- octostar/models/insert_entity_relationships_item.py +107 -0
- octostar/models/insert_entity_request.py +94 -0
- octostar/models/internal_server_error.py +82 -0
- octostar/models/job_execution_result.py +146 -0
- octostar/models/job_status.py +196 -0
- octostar/models/job_status_labels.py +60 -0
- octostar/models/job_with_url.py +82 -0
- octostar/models/kill_job_response_401.py +74 -0
- octostar/models/list_app_jobs_response_401.py +74 -0
- octostar/models/list_app_jobs_response_500.py +82 -0
- octostar/models/list_apps_response_401.py +74 -0
- octostar/models/list_apps_response_500.py +82 -0
- octostar/models/multi_query_json_body.py +100 -0
- octostar/models/multi_query_json_body_queries_item.py +80 -0
- octostar/models/multi_query_response_400.py +82 -0
- octostar/models/multi_query_response_401.py +74 -0
- octostar/models/not_found_error.py +74 -0
- octostar/models/octostar_event.py +96 -0
- octostar/models/octostar_event_octostar_payload.py +100 -0
- octostar/models/octostar_event_octostar_payload_level.py +11 -0
- octostar/models/os_notification.py +122 -0
- octostar/models/processing_status.py +262 -0
- octostar/models/processing_status_code.py +14 -0
- octostar/models/progress_request.py +73 -0
- octostar/models/publish_notification_response_401.py +74 -0
- octostar/models/pull_events_from_stream_response_401.py +74 -0
- octostar/models/push_event_to_stream_response_401.py +74 -0
- octostar/models/query_json_body.py +101 -0
- octostar/models/query_json_body_params.py +60 -0
- octostar/models/query_response_400.py +82 -0
- octostar/models/query_response_401.py +74 -0
- octostar/models/set_job_progress_response_401.py +74 -0
- octostar/models/string_to_value_label_map.py +99 -0
- octostar/models/string_to_value_label_map_data.py +89 -0
- octostar/models/string_to_value_label_map_data_additional_property.py +80 -0
- octostar/models/successful_get_tags.py +103 -0
- octostar/models/successful_insertion.py +98 -0
- octostar/models/tag_entities_response_401.py +74 -0
- octostar/models/toast_level.py +11 -0
- octostar/models/toast_response_401.py +74 -0
- octostar/models/undeploy_app_response_401.py +74 -0
- octostar/models/update_processing_status_response_200.py +82 -0
- octostar/models/update_processing_status_response_400.py +82 -0
- octostar/models/update_processing_status_response_500.py +82 -0
- octostar/models/upsert_entities_response_401.py +74 -0
- octostar/models/upsert_entity.py +114 -0
- octostar/models/upsert_entity_base.py +266 -0
- octostar/models/upsert_entity_relationships_item.py +107 -0
- octostar/py.typed +1 -0
- octostar/types.py +54 -0
- octostar/utils/__init__.py +15 -0
- octostar/utils/chat/__init__.py +0 -0
- octostar/utils/chat/chat.py +513 -0
- octostar/utils/chat/detokenize.py +105 -0
- octostar/utils/chat/get_default_model.py +50 -0
- octostar/utils/chat/list_models.py +91 -0
- octostar/utils/chat/tokenize.py +105 -0
- octostar/utils/commons.py +226 -0
- octostar/utils/exceptions.py +134 -0
- octostar/utils/jobs/__init__.py +0 -0
- octostar/utils/jobs/apps/__init__.py +0 -0
- octostar/utils/jobs/apps/deploy_app.py +81 -0
- octostar/utils/jobs/apps/execute_app_job.py +114 -0
- octostar/utils/jobs/apps/get_app_logs.py +113 -0
- octostar/utils/jobs/apps/get_app_secret.py +102 -0
- octostar/utils/jobs/apps/get_apps_url.py +73 -0
- octostar/utils/jobs/apps/list_app_jobs.py +62 -0
- octostar/utils/jobs/apps/list_apps.py +126 -0
- octostar/utils/jobs/apps/undeploy_app.py +48 -0
- octostar/utils/jobs/get_job_logs.py +113 -0
- octostar/utils/jobs/get_job_progress.py +76 -0
- octostar/utils/jobs/kill_job.py +47 -0
- octostar/utils/jobs/set_job_progress.py +67 -0
- octostar/utils/meta/__init__.py +0 -0
- octostar/utils/meta/get_version.py +30 -0
- octostar/utils/meta/get_whoami.py +30 -0
- octostar/utils/notifications/__init__.py +0 -0
- octostar/utils/notifications/delete_stream.py +58 -0
- octostar/utils/notifications/get_my_subscriptions.py +49 -0
- octostar/utils/notifications/publish_notification.py +73 -0
- octostar/utils/notifications/pull_event_from_stream.py +63 -0
- octostar/utils/notifications/pull_events_from_stream.py +64 -0
- octostar/utils/notifications/push_event_to_stream.py +109 -0
- octostar/utils/notifications/push_events_to_stream.py +137 -0
- octostar/utils/notifications/toast.py +92 -0
- octostar/utils/ontology/__init__.py +10 -0
- octostar/utils/ontology/fetch_ontology_data.py +141 -0
- octostar/utils/ontology/get_ontologies.py +55 -0
- octostar/utils/ontology/multiquery_ontology.py +287 -0
- octostar/utils/ontology/query_ontology.py +186 -0
- octostar/utils/pipeline/__init__.py +1 -0
- octostar/utils/pipeline/get_processing_status.py +230 -0
- octostar/utils/pipeline/update_processing_status.py +286 -0
- octostar/utils/search/__init__.py +11 -0
- octostar/utils/search/bulk_update.py +138 -0
- octostar/utils/search/count.py +117 -0
- octostar/utils/search/get_entity_annotations.py +304 -0
- octostar/utils/search/get_index_definition.py +111 -0
- octostar/utils/search/multi_search.py +129 -0
- octostar/utils/workspace/__init__.py +0 -0
- octostar/utils/workspace/delete_entities.py +247 -0
- octostar/utils/workspace/delete_entity.py +81 -0
- octostar/utils/workspace/delete_relationship.py +78 -0
- octostar/utils/workspace/delete_relationships.py +85 -0
- octostar/utils/workspace/delete_temporary_blob.py +85 -0
- octostar/utils/workspace/extract_entities.py +140 -0
- octostar/utils/workspace/get_filepath_from_item.py +85 -0
- octostar/utils/workspace/get_filepaths_from_items.py +100 -0
- octostar/utils/workspace/get_files_tree.py +102 -0
- octostar/utils/workspace/get_item_from_filepath.py +102 -0
- octostar/utils/workspace/get_items_from_filepaths.py +108 -0
- octostar/utils/workspace/linkcharts/__init__.py +0 -0
- octostar/utils/workspace/linkcharts/create_linkchart.py +241 -0
- octostar/utils/workspace/permissions/PermissionLevel.py +8 -0
- octostar/utils/workspace/permissions/__init__.py +1 -0
- octostar/utils/workspace/permissions/get_permissions.py +81 -0
- octostar/utils/workspace/read_attachment.py +284 -0
- octostar/utils/workspace/read_file.py +113 -0
- octostar/utils/workspace/read_temporary_blob.py +428 -0
- octostar/utils/workspace/saved_searches/__init__.py +0 -0
- octostar/utils/workspace/saved_searches/create_saved_search.py +183 -0
- octostar/utils/workspace/tags/__init__.py +0 -0
- octostar/utils/workspace/tags/delete_tag_from_entities.py +96 -0
- octostar/utils/workspace/tags/tag_entities.py +175 -0
- octostar/utils/workspace/upsert_entities.py +268 -0
- octostar/utils/workspace/upsert_entity.py +110 -0
- octostar/utils/workspace/upsert_relationship.py +128 -0
- octostar/utils/workspace/upsert_relationships.py +194 -0
- octostar/utils/workspace/write_attachment.py +263 -0
- octostar/utils/workspace/write_file.py +335 -0
- octostar/utils/workspace/write_temporary_blob.py +218 -0
- octostar_python_client-0.1.759.dist-info/METADATA +159 -0
- octostar_python_client-0.1.759.dist-info/RECORD +257 -0
- octostar_python_client-0.1.759.dist-info/WHEEL +5 -0
- octostar_python_client-0.1.759.dist-info/licenses/LICENSE +21 -0
- octostar_python_client-0.1.759.dist-info/top_level.txt +1 -0
octostar/client.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""
|
|
2
|
+
client.py
|
|
3
|
+
|
|
4
|
+
This module provides functionalities to impersonate users for access with the OctoStar API.
|
|
5
|
+
User instances contain info like name, email, jwt-related data and, crucially, a client to execute API calls with.
|
|
6
|
+
A default user is provided via as_launching_user() (the user who launched the app).
|
|
7
|
+
|
|
8
|
+
Be careful! Imported modules in streamlit share their variables and functions between threads (= user sessions),
|
|
9
|
+
so you cannot pass around clients between modules of your code!
|
|
10
|
+
|
|
11
|
+
Example usage:
|
|
12
|
+
--------------
|
|
13
|
+
- Manually creating and using a client:
|
|
14
|
+
client = make_client(my_fixed_jwt, my_os_api_endpoint, my_ontology_name)
|
|
15
|
+
client.execute(query_ontology.sync, my_sql_query)
|
|
16
|
+
|
|
17
|
+
- Using a client with the context manager (passing the default client):
|
|
18
|
+
with as_launching_user() as client:
|
|
19
|
+
client.execute(query_ontology.sync, my_sql_query)
|
|
20
|
+
|
|
21
|
+
- Using a client via the default client decorator:
|
|
22
|
+
@impersonating_launching_user()
|
|
23
|
+
def my_function(client):
|
|
24
|
+
client.execute(query_ontology.sync, my_sql_query)
|
|
25
|
+
|
|
26
|
+
- Wrapper for local development via flag:
|
|
27
|
+
@impersonating_launching_user()
|
|
28
|
+
@dev_mode(dev_flag=os.environ.get('OS_DEV_MODE'), fixed_jwt=os.environ.get('OS_JWT'), api_endpoint=os.environ.get('OS_API_ENDPOINT'), ontology_name=os.environ.get('OS_ONTOLOGY'))
|
|
29
|
+
def my_function(client):
|
|
30
|
+
client.execute(query_ontology.sync, my_sql_query)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import base64
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import ssl
|
|
37
|
+
import time
|
|
38
|
+
import re
|
|
39
|
+
from urllib.parse import urlparse, urlunparse
|
|
40
|
+
import jwt
|
|
41
|
+
import logging
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any, Callable, Dict, Union, List, TypeVar
|
|
44
|
+
from functools import wraps
|
|
45
|
+
import attr
|
|
46
|
+
import hashlib
|
|
47
|
+
from warnings import warn
|
|
48
|
+
|
|
49
|
+
# Streamlit imports are lazy-loaded when needed
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
logger.setLevel(logging.DEBUG)
|
|
53
|
+
|
|
54
|
+
T = TypeVar("T")
|
|
55
|
+
|
|
56
|
+
OS_JWT_SECRET_PATH = "/etc/secrets/OS_JWT"
|
|
57
|
+
REQUIRED_ENV_VARS = ["OS_API_ENDPOINT", "OS_ONTOLOGY"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@attr.s(auto_attribs=True)
|
|
61
|
+
class Client:
|
|
62
|
+
"""A class for keeping track of data related to the API
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
base_url: The base URL for the API, all requests are made to a relative path to this URL
|
|
66
|
+
cookies: A dictionary of cookies to be sent with every request
|
|
67
|
+
headers: A dictionary of headers to be sent with every request
|
|
68
|
+
timeout: The maximum amount of a time in seconds a request can take. API functions will raise
|
|
69
|
+
httpx.TimeoutException if this is exceeded.
|
|
70
|
+
verify_ssl: Whether or not to verify the SSL certificate of the API server. This should be True in production,
|
|
71
|
+
but can be set to False for testing purposes.
|
|
72
|
+
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
|
|
73
|
+
status code that was not documented in the source OpenAPI document.
|
|
74
|
+
follow_redirects: Whether or not to follow redirects. Default value is False.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
base_url: str
|
|
78
|
+
cookies: Dict[str, str] = attr.ib(factory=dict, kw_only=True)
|
|
79
|
+
headers: Dict[str, str] = attr.ib(factory=dict, kw_only=True)
|
|
80
|
+
timeout: float = attr.ib(5.0, kw_only=True)
|
|
81
|
+
verify_ssl: Union[str, bool, ssl.SSLContext] = attr.ib(True, kw_only=True)
|
|
82
|
+
raise_on_unexpected_status: bool = attr.ib(False, kw_only=True)
|
|
83
|
+
follow_redirects: bool = attr.ib(False, kw_only=True)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def ontology(self) -> str:
|
|
87
|
+
return self.headers["x-ontology"]
|
|
88
|
+
|
|
89
|
+
@ontology.setter
|
|
90
|
+
def ontology(self, value) -> None:
|
|
91
|
+
self.headers["x-ontology"] = value
|
|
92
|
+
|
|
93
|
+
def get_headers(self) -> Dict[str, str]:
|
|
94
|
+
"""Get headers to be used in all endpoints"""
|
|
95
|
+
return {**self.headers}
|
|
96
|
+
|
|
97
|
+
def with_headers(self, headers: Dict[str, str]) -> "Client":
|
|
98
|
+
"""Get a new client matching this one with additional headers"""
|
|
99
|
+
return attr.evolve(self, headers={**self.headers, **headers})
|
|
100
|
+
|
|
101
|
+
def get_cookies(self) -> Dict[str, str]:
|
|
102
|
+
return {**self.cookies}
|
|
103
|
+
|
|
104
|
+
def with_cookies(self, cookies: Dict[str, str]) -> "Client":
|
|
105
|
+
"""Get a new client matching this one with additional cookies"""
|
|
106
|
+
return attr.evolve(self, cookies={**self.cookies, **cookies})
|
|
107
|
+
|
|
108
|
+
def get_timeout(self) -> float:
|
|
109
|
+
return self.timeout
|
|
110
|
+
|
|
111
|
+
def with_timeout(self, timeout: float) -> "Client":
|
|
112
|
+
"""Get a new client matching this one with a new timeout (in seconds)"""
|
|
113
|
+
return attr.evolve(self, timeout=timeout)
|
|
114
|
+
|
|
115
|
+
def execute(self, callable_fn: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
|
116
|
+
"""Execute an OS API call passing the client as ourselves."""
|
|
117
|
+
return callable_fn(*args, **{**kwargs, "client": self})
|
|
118
|
+
|
|
119
|
+
def get_base_url_v1(self):
|
|
120
|
+
return self.base_url.rstrip("/")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def fetch_token_from_file() -> Union[str, None]:
|
|
124
|
+
token_file = Path(OS_JWT_SECRET_PATH)
|
|
125
|
+
max_attempts = 10
|
|
126
|
+
attempts = 0
|
|
127
|
+
|
|
128
|
+
while attempts < max_attempts:
|
|
129
|
+
try:
|
|
130
|
+
if token_file.is_file():
|
|
131
|
+
return token_file.read_text().strip()
|
|
132
|
+
else:
|
|
133
|
+
print(f"{OS_JWT_SECRET_PATH} not found. Retrying...")
|
|
134
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
135
|
+
print(f"Error reading {OS_JWT_SECRET_PATH}: {e}. Retrying...")
|
|
136
|
+
|
|
137
|
+
time.sleep(1)
|
|
138
|
+
attempts += 1
|
|
139
|
+
|
|
140
|
+
print("Maximum number of attempts reached. Giving up.")
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_valid_jwt(token: str) -> bool:
|
|
145
|
+
try:
|
|
146
|
+
header, payload, signature = token.split(".")
|
|
147
|
+
decoded_payload = base64.urlsafe_b64decode(payload + "==").decode("utf-8")
|
|
148
|
+
payload_data = json.loads(decoded_payload)
|
|
149
|
+
if "exp" in payload_data and payload_data["exp"] > time.time():
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
except (ValueError, json.JSONDecodeError, KeyError):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@attr.s(auto_attribs=True)
|
|
157
|
+
class AuthenticatedClient(Client):
|
|
158
|
+
"""A Client which has been authenticated for use on secured endpoints"""
|
|
159
|
+
|
|
160
|
+
fixed_token: str = None
|
|
161
|
+
prefix: str = "Bearer"
|
|
162
|
+
auth_header_name: str = "Authorization"
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def token(self) -> str:
|
|
166
|
+
if self.fixed_token:
|
|
167
|
+
return self.fixed_token
|
|
168
|
+
|
|
169
|
+
token = fetch_token_from_file()
|
|
170
|
+
if token and is_valid_jwt(token):
|
|
171
|
+
os.environ["OS_JWT"] = (
|
|
172
|
+
f"DEPRECATED: use '{OS_JWT_SECRET_PATH}' instead of the environment variable"
|
|
173
|
+
)
|
|
174
|
+
return token
|
|
175
|
+
|
|
176
|
+
print("WARNING: no valid secret found for OS_JWT!")
|
|
177
|
+
|
|
178
|
+
token = os.environ.get("OS_JWT")
|
|
179
|
+
if token and is_valid_jwt(token):
|
|
180
|
+
print(
|
|
181
|
+
"WARNING: Using OS_JWT from environment variable: use this for developer mode only!"
|
|
182
|
+
)
|
|
183
|
+
self.fixed_token = token
|
|
184
|
+
return token
|
|
185
|
+
|
|
186
|
+
raise RuntimeError("OS_JWT not found")
|
|
187
|
+
|
|
188
|
+
def get_headers(self) -> Dict[str, str]:
|
|
189
|
+
"""Get headers to be used in authenticated endpoints"""
|
|
190
|
+
auth_header_value = f"{self.prefix} {self.token}" if self.prefix else self.token
|
|
191
|
+
return {self.auth_header_name: auth_header_value, **self.headers}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class User(object):
|
|
195
|
+
"""A User with an associated Client"""
|
|
196
|
+
|
|
197
|
+
def __init__(self, client):
|
|
198
|
+
self.client = client
|
|
199
|
+
|
|
200
|
+
def decode_jwt(self):
|
|
201
|
+
return jwt.decode(
|
|
202
|
+
self.client.token, algorithms=["ES256"], options={"verify_signature": False}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def username(self):
|
|
207
|
+
return self.decode_jwt()["username"]
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def email(self):
|
|
211
|
+
return self.decode_jwt()["email"]
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def roles(self):
|
|
215
|
+
return self.decode_jwt()["roles"]
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def jwt(self):
|
|
219
|
+
return self.client.token
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def ontology(self):
|
|
223
|
+
return self.client.ontology
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def jwt_issuer(self):
|
|
227
|
+
return self.decode_jwt()["iss"]
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def jwt_issued_at(self):
|
|
231
|
+
return self.decode_jwt()["iat"]
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def jwt_expires_at(self):
|
|
235
|
+
return self.decode_jwt()["exp"]
|
|
236
|
+
|
|
237
|
+
def __eq__(self, other):
|
|
238
|
+
if not isinstance(other, User):
|
|
239
|
+
return NotImplemented
|
|
240
|
+
return self.client.token == other.client.token
|
|
241
|
+
|
|
242
|
+
def __hash__(self):
|
|
243
|
+
return int(hashlib.md5(self.client.token.encode("utf-8")).hexdigest(), 16)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class UserContext:
|
|
247
|
+
def __init__(self, user: User):
|
|
248
|
+
self.user = user
|
|
249
|
+
|
|
250
|
+
def __enter__(self):
|
|
251
|
+
return self.user.client
|
|
252
|
+
|
|
253
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def check_required_env_vars(required_vars: List[str]):
|
|
258
|
+
missing_vars = [var for var in required_vars if not os.environ.get(var)]
|
|
259
|
+
if missing_vars:
|
|
260
|
+
raise EnvironmentError(
|
|
261
|
+
f"Missing required environment variables: {', '.join(missing_vars)}. Please set these variables and try again."
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def make_client(
|
|
266
|
+
fixed_jwt=None,
|
|
267
|
+
api_endpoint=None,
|
|
268
|
+
ontology_name=None,
|
|
269
|
+
timeout=90,
|
|
270
|
+
follow_redirects=True,
|
|
271
|
+
) -> AuthenticatedClient:
|
|
272
|
+
local_vars = []
|
|
273
|
+
if ontology_name:
|
|
274
|
+
local_vars.append("OS_ONTOLOGY")
|
|
275
|
+
if api_endpoint:
|
|
276
|
+
local_vars.append("OS_API_ENDPOINT")
|
|
277
|
+
|
|
278
|
+
required_env_vars = list(set(REQUIRED_ENV_VARS).difference(set(local_vars)))
|
|
279
|
+
check_required_env_vars(required_env_vars)
|
|
280
|
+
|
|
281
|
+
if not ontology_name:
|
|
282
|
+
ontology_name = os.environ["OS_ONTOLOGY"]
|
|
283
|
+
if not api_endpoint:
|
|
284
|
+
api_endpoint = os.environ["OS_API_ENDPOINT"]
|
|
285
|
+
|
|
286
|
+
ancestor = os.environ.get("OS_ANCESTOR")
|
|
287
|
+
current_pod_name = os.environ.get("OS_CURRENT_POD_NAME")
|
|
288
|
+
if not ancestor and current_pod_name:
|
|
289
|
+
ancestor = current_pod_name[:-6]
|
|
290
|
+
if not ancestor:
|
|
291
|
+
ancestor = "local-dev"
|
|
292
|
+
app_name = os.environ.get("OS_APP_NAME", "unknown-local-app")
|
|
293
|
+
|
|
294
|
+
return AuthenticatedClient(
|
|
295
|
+
fixed_token=fixed_jwt,
|
|
296
|
+
timeout=timeout,
|
|
297
|
+
base_url=api_endpoint,
|
|
298
|
+
headers={
|
|
299
|
+
"x-ontology": ontology_name,
|
|
300
|
+
"x-app-name": app_name,
|
|
301
|
+
"x-ancestor": ancestor,
|
|
302
|
+
},
|
|
303
|
+
follow_redirects=follow_redirects,
|
|
304
|
+
verify_ssl=True,
|
|
305
|
+
raise_on_unexpected_status=False,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def as_launching_user():
|
|
310
|
+
launching_user = User(make_client())
|
|
311
|
+
return UserContext(launching_user)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def as_developer_user(fixed_jwt=None, api_endpoint=None, ontology_name=None, **kwargs):
|
|
315
|
+
warn("A developer user has been instantiated!", Warning, stacklevel=2)
|
|
316
|
+
developer_user = User(make_client(fixed_jwt, api_endpoint, ontology_name, **kwargs))
|
|
317
|
+
return UserContext(developer_user)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def impersonating_launching_user(**client_kwargs):
|
|
321
|
+
def decorator(func):
|
|
322
|
+
@wraps(func)
|
|
323
|
+
def wrapper(*args, **kwargs):
|
|
324
|
+
client = as_launching_user(**client_kwargs).user.client
|
|
325
|
+
kwargs["client"] = client
|
|
326
|
+
return func(*args, **kwargs)
|
|
327
|
+
|
|
328
|
+
return wrapper
|
|
329
|
+
|
|
330
|
+
return decorator
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def impersonating_developer_user(**client_kwargs):
|
|
334
|
+
def decorator(func):
|
|
335
|
+
@wraps(func)
|
|
336
|
+
def wrapper(*args, **kwargs):
|
|
337
|
+
client = as_developer_user(**client_kwargs).user.client
|
|
338
|
+
kwargs["client"] = client
|
|
339
|
+
return func(*args, **kwargs)
|
|
340
|
+
|
|
341
|
+
return wrapper
|
|
342
|
+
|
|
343
|
+
return decorator
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def dev_mode(dev_flag, **kwargs):
|
|
347
|
+
def decorator(func):
|
|
348
|
+
if f"{dev_flag}".lower() == "true":
|
|
349
|
+
return impersonating_developer_user(**kwargs)(func)
|
|
350
|
+
else:
|
|
351
|
+
return func
|
|
352
|
+
|
|
353
|
+
return decorator
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
custom_default_client = None
|
|
357
|
+
|
|
358
|
+
client_missing_msg = """This function is deprecated and will be removed soon.
|
|
359
|
+
Please create a client instead using octostar.client.make_client() and run the function via client.execute()."""
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def set_default_client(client):
|
|
363
|
+
global custom_default_client
|
|
364
|
+
warn(client_missing_msg, FutureWarning, stacklevel=2)
|
|
365
|
+
custom_default_client = client
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def get_default_client():
|
|
369
|
+
global custom_default_client
|
|
370
|
+
warn(client_missing_msg, FutureWarning, stacklevel=2)
|
|
371
|
+
if not custom_default_client:
|
|
372
|
+
custom_default_client = make_client()
|
|
373
|
+
return custom_default_client
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _is_jwt_expired_or_expiring_soon(jwt_token, buffer_seconds=300):
|
|
377
|
+
"""Check if JWT is expired or expiring within buffer_seconds (default 5 minutes)"""
|
|
378
|
+
try:
|
|
379
|
+
# Decode without verification to check expiration
|
|
380
|
+
decoded = jwt.decode(jwt_token, options={"verify_signature": False})
|
|
381
|
+
exp = decoded.get("exp")
|
|
382
|
+
if not exp:
|
|
383
|
+
logger.debug("JWT has no expiration claim, treating as expired")
|
|
384
|
+
return True # No expiration claim, treat as expired
|
|
385
|
+
|
|
386
|
+
current_time = int(time.time())
|
|
387
|
+
expires_in = exp - current_time
|
|
388
|
+
is_expired = current_time >= (exp - buffer_seconds)
|
|
389
|
+
|
|
390
|
+
logger.debug(
|
|
391
|
+
f"JWT expires in {expires_in}s, buffer={buffer_seconds}s, is_expired={is_expired}"
|
|
392
|
+
)
|
|
393
|
+
return is_expired
|
|
394
|
+
except Exception as e:
|
|
395
|
+
logger.debug(f"Failed to decode JWT: {e}, treating as expired")
|
|
396
|
+
return True # If we can't decode, treat as expired
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _should_refresh_user(is_first_time, force_refresh, jwt_expired, prev_user_exists):
|
|
400
|
+
"""Determine if user/JWT should be refreshed"""
|
|
401
|
+
if is_first_time:
|
|
402
|
+
logger.debug("Refreshing user: first time")
|
|
403
|
+
return True
|
|
404
|
+
if force_refresh:
|
|
405
|
+
logger.debug("Refreshing user: force_refresh=True")
|
|
406
|
+
return True
|
|
407
|
+
if jwt_expired:
|
|
408
|
+
logger.debug("Refreshing user: JWT expired")
|
|
409
|
+
return True
|
|
410
|
+
if not prev_user_exists:
|
|
411
|
+
logger.debug("Refreshing user: no previous user cached")
|
|
412
|
+
return True
|
|
413
|
+
|
|
414
|
+
logger.debug("Not refreshing user: using cached user")
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def impersonating_running_user(**client_kwargs):
|
|
419
|
+
def decorator(func):
|
|
420
|
+
@wraps(func)
|
|
421
|
+
def wrapper(*args, **kwargs):
|
|
422
|
+
client = as_running_user(**client_kwargs).user.client
|
|
423
|
+
kwargs["client"] = client
|
|
424
|
+
return func(*args, **kwargs)
|
|
425
|
+
|
|
426
|
+
return wrapper
|
|
427
|
+
|
|
428
|
+
return decorator
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def as_running_user(force_refresh=False):
|
|
432
|
+
try:
|
|
433
|
+
from octostar_streamlit.desktop import Whoami
|
|
434
|
+
import streamlit as st
|
|
435
|
+
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
|
436
|
+
except ImportError as e:
|
|
437
|
+
raise RuntimeError(
|
|
438
|
+
"Running user integration requires the 'streamlit' extra. "
|
|
439
|
+
"Install with `pip install octostar-python-client[streamlit]`."
|
|
440
|
+
) from e
|
|
441
|
+
|
|
442
|
+
script_ctx = get_script_run_ctx()
|
|
443
|
+
internal_st_key = "__run_user"
|
|
444
|
+
if internal_st_key not in st.session_state:
|
|
445
|
+
st.session_state[internal_st_key] = dict()
|
|
446
|
+
prev_run = st.session_state[internal_st_key].get("prev_run")
|
|
447
|
+
prev_user = st.session_state[internal_st_key].get("prev_user")
|
|
448
|
+
prev_jwt = st.session_state[internal_st_key].get("prev_jwt")
|
|
449
|
+
is_first_time = False
|
|
450
|
+
if prev_run is not script_ctx.script_requests: # this changes at every st rerun
|
|
451
|
+
is_first_time = True
|
|
452
|
+
st.session_state[internal_st_key]["prev_run"] = script_ctx.script_requests
|
|
453
|
+
|
|
454
|
+
logger.debug(
|
|
455
|
+
f"as_running_user called: force_refresh={force_refresh}, is_first_time={is_first_time}, has_prev_user={bool(prev_user)}, has_prev_jwt={bool(prev_jwt)}"
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Check if JWT is expired or expiring soon
|
|
459
|
+
jwt_expired = prev_jwt and _is_jwt_expired_or_expiring_soon(prev_jwt)
|
|
460
|
+
|
|
461
|
+
# Determine if we should refresh
|
|
462
|
+
should_refresh = _should_refresh_user(
|
|
463
|
+
is_first_time, force_refresh, jwt_expired, bool(prev_user)
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if not should_refresh:
|
|
467
|
+
logger.debug("Returning cached user")
|
|
468
|
+
return UserContext(prev_user)
|
|
469
|
+
|
|
470
|
+
logger.debug("Fetching new user from Whoami()")
|
|
471
|
+
running_user = Whoami()
|
|
472
|
+
running_user_jwt = running_user.os_jwt
|
|
473
|
+
if not running_user:
|
|
474
|
+
if prev_user and not jwt_expired:
|
|
475
|
+
logger.debug("Whoami() failed but using cached user (JWT not expired)")
|
|
476
|
+
return UserContext(prev_user)
|
|
477
|
+
else:
|
|
478
|
+
logger.debug("Whoami() failed and no valid cached user, stopping")
|
|
479
|
+
st.stop()
|
|
480
|
+
|
|
481
|
+
running_user_hash = int(
|
|
482
|
+
hashlib.md5(running_user_jwt.encode("utf-8")).hexdigest(), 16
|
|
483
|
+
)
|
|
484
|
+
if not prev_user or hash(prev_user) != running_user_hash or jwt_expired:
|
|
485
|
+
logger.debug("Creating new client and user")
|
|
486
|
+
client = make_client(fixed_jwt=running_user_jwt)
|
|
487
|
+
user = User(client)
|
|
488
|
+
st.session_state[internal_st_key]["prev_user"] = user
|
|
489
|
+
st.session_state[internal_st_key]["prev_jwt"] = running_user_jwt
|
|
490
|
+
|
|
491
|
+
logger.debug("Returning user context")
|
|
492
|
+
return UserContext(user)
|
octostar/errors.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Contains shared errors types that can be raised from API functions"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnexpectedStatus(Exception):
|
|
5
|
+
"""Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True"""
|
|
6
|
+
|
|
7
|
+
def __init__(self, status_code: int, content: bytes):
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
self.content = content
|
|
10
|
+
|
|
11
|
+
super().__init__(f"Unexpected status code: {status_code}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DeprecatedEndpointError(Exception):
|
|
15
|
+
"""Raised when calling an endpoint that has been permanently removed.
|
|
16
|
+
|
|
17
|
+
This error indicates that the endpoint has been removed and will not be implemented
|
|
18
|
+
in the new API. The functionality may have been replaced by a different approach.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, endpoint_name: str, message: str = None):
|
|
22
|
+
self.endpoint_name = endpoint_name
|
|
23
|
+
self.message = (
|
|
24
|
+
message or f"Endpoint '{endpoint_name}' has been permanently removed."
|
|
25
|
+
)
|
|
26
|
+
super().__init__(self.message)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class V1MigrationPendingError(Exception):
|
|
30
|
+
"""Raised when calling an endpoint that is not yet available in the FastAPI v1 API.
|
|
31
|
+
|
|
32
|
+
This error indicates that the endpoint migration is pending and will be implemented
|
|
33
|
+
in a future release. Check the error message for the expected v1 endpoint path.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, endpoint_name: str, v1_endpoint: str = None, message: str = None
|
|
38
|
+
):
|
|
39
|
+
self.endpoint_name = endpoint_name
|
|
40
|
+
self.v1_endpoint = v1_endpoint
|
|
41
|
+
if message:
|
|
42
|
+
self.message = message
|
|
43
|
+
elif v1_endpoint:
|
|
44
|
+
self.message = f"Endpoint '{endpoint_name}' migration pending. Will be available at '{v1_endpoint}'."
|
|
45
|
+
else:
|
|
46
|
+
self.message = f"Endpoint '{endpoint_name}' migration to v1 API is pending."
|
|
47
|
+
super().__init__(self.message)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["UnexpectedStatus", "DeprecatedEndpointError", "V1MigrationPendingError"]
|