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.
Files changed (257) hide show
  1. octostar/__init__.py +9 -0
  2. octostar/api/__init__.py +1 -0
  3. octostar/api/apps/__init__.py +0 -0
  4. octostar/api/apps/deploy_app.py +210 -0
  5. octostar/api/apps/execute_app_job.py +188 -0
  6. octostar/api/apps/get_app_logs.py +210 -0
  7. octostar/api/apps/get_apps_url.py +188 -0
  8. octostar/api/apps/get_job_logs.py +210 -0
  9. octostar/api/apps/get_job_progress.py +162 -0
  10. octostar/api/apps/kill_job.py +160 -0
  11. octostar/api/apps/list_app_jobs.py +276 -0
  12. octostar/api/apps/list_apps.py +251 -0
  13. octostar/api/apps/set_job_progress.py +216 -0
  14. octostar/api/apps/undeploy_app.py +160 -0
  15. octostar/api/metadata/__init__.py +0 -0
  16. octostar/api/metadata/get_version.py +232 -0
  17. octostar/api/metadata/get_whoami.py +232 -0
  18. octostar/api/notifications/__init__.py +0 -0
  19. octostar/api/notifications/delete_stream.py +222 -0
  20. octostar/api/notifications/get_subscriptions.py +240 -0
  21. octostar/api/notifications/publish_notification.py +275 -0
  22. octostar/api/notifications/pull_events_from_stream.py +282 -0
  23. octostar/api/notifications/push_event_to_stream.py +265 -0
  24. octostar/api/notifications/toast.py +264 -0
  25. octostar/api/ontology/__init__.py +0 -0
  26. octostar/api/ontology/fetch_ontology_data.py +275 -0
  27. octostar/api/ontology/get_ontologies.py +237 -0
  28. octostar/api/ontology/multi_query.py +297 -0
  29. octostar/api/ontology/query.py +276 -0
  30. octostar/api/pipeline/__init__.py +1 -0
  31. octostar/api/pipeline/get_processing_status.py +185 -0
  32. octostar/api/pipeline/update_processing_status.py +164 -0
  33. octostar/api/search/__init__.py +0 -0
  34. octostar/api/search/get_annotations.py +153 -0
  35. octostar/api/workspace_data/__init__.py +0 -0
  36. octostar/api/workspace_data/delete_blob.py +212 -0
  37. octostar/api/workspace_data/delete_entities.py +326 -0
  38. octostar/api/workspace_data/download_blob.py +235 -0
  39. octostar/api/workspace_data/get_attachment.py +336 -0
  40. octostar/api/workspace_data/get_files_tree.py +397 -0
  41. octostar/api/workspace_data/upload_blob.py +235 -0
  42. octostar/api/workspace_data/upsert_entities.py +284 -0
  43. octostar/api/workspace_permissions/__init__.py +0 -0
  44. octostar/api/workspace_permissions/get_permissions.py +325 -0
  45. octostar/api/workspace_tags/__init__.py +0 -0
  46. octostar/api/workspace_tags/delete_tag_from_entities.py +141 -0
  47. octostar/api/workspace_tags/tag_entities.py +180 -0
  48. octostar/client.py +492 -0
  49. octostar/errors.py +50 -0
  50. octostar/models/__init__.py +249 -0
  51. octostar/models/acknowledgement.py +74 -0
  52. octostar/models/acknowledgement_with_data.py +82 -0
  53. octostar/models/app_status.py +239 -0
  54. octostar/models/app_status_annotations.py +66 -0
  55. octostar/models/app_status_labels.py +69 -0
  56. octostar/models/app_with_url.py +82 -0
  57. octostar/models/child_processing_status.py +118 -0
  58. octostar/models/delete_entities_response_401.py +74 -0
  59. octostar/models/delete_entities_response_409.py +82 -0
  60. octostar/models/delete_entities_response_500.py +82 -0
  61. octostar/models/delete_stream_response_401.py +74 -0
  62. octostar/models/delete_tag_from_entities_response_401.py +74 -0
  63. octostar/models/deploy_app_json_body.py +90 -0
  64. octostar/models/deploy_app_json_body_secrets.py +65 -0
  65. octostar/models/deploy_app_response_200.py +98 -0
  66. octostar/models/deploy_app_response_200_data.py +60 -0
  67. octostar/models/deploy_app_response_400.py +82 -0
  68. octostar/models/deploy_app_response_403.py +82 -0
  69. octostar/models/deploy_app_response_404.py +82 -0
  70. octostar/models/deploy_app_response_409.py +82 -0
  71. octostar/models/deploy_app_response_500.py +82 -0
  72. octostar/models/entity.py +80 -0
  73. octostar/models/entity_response.py +99 -0
  74. octostar/models/entity_response_s3_urls.py +93 -0
  75. octostar/models/entity_response_s3_urls_additional_property.py +105 -0
  76. octostar/models/entity_response_s3_urls_additional_property_fields.py +114 -0
  77. octostar/models/execute_app_job_json_body.py +151 -0
  78. octostar/models/execute_app_job_json_body_annotation.py +65 -0
  79. octostar/models/execute_app_job_response_401.py +74 -0
  80. octostar/models/fetch_ontology_data_response_200.py +60 -0
  81. octostar/models/fetch_ontology_data_response_401.py +74 -0
  82. octostar/models/fetch_ontology_data_response_500.py +82 -0
  83. octostar/models/get_app_logs_response_401.py +74 -0
  84. octostar/models/get_app_logs_response_404.py +74 -0
  85. octostar/models/get_app_logs_response_500.py +82 -0
  86. octostar/models/get_apps_url_json_body.py +76 -0
  87. octostar/models/get_apps_url_response_401.py +74 -0
  88. octostar/models/get_apps_url_response_500.py +82 -0
  89. octostar/models/get_attachment_response_200.py +74 -0
  90. octostar/models/get_attachment_response_401.py +74 -0
  91. octostar/models/get_files_tree_response_200.py +106 -0
  92. octostar/models/get_files_tree_response_200_status.py +8 -0
  93. octostar/models/get_files_tree_response_400.py +111 -0
  94. octostar/models/get_files_tree_response_400_data.py +60 -0
  95. octostar/models/get_files_tree_response_400_status.py +8 -0
  96. octostar/models/get_files_tree_response_401.py +74 -0
  97. octostar/models/get_files_tree_response_500.py +111 -0
  98. octostar/models/get_files_tree_response_500_data.py +60 -0
  99. octostar/models/get_files_tree_response_500_status.py +8 -0
  100. octostar/models/get_job_logs_response_401.py +74 -0
  101. octostar/models/get_job_logs_response_404.py +74 -0
  102. octostar/models/get_job_logs_response_500.py +82 -0
  103. octostar/models/get_job_progress_response_401.py +74 -0
  104. octostar/models/get_object_response_401.py +74 -0
  105. octostar/models/get_ontologies_response_401.py +74 -0
  106. octostar/models/get_ontologies_response_500.py +81 -0
  107. octostar/models/get_permissions_response_200.py +98 -0
  108. octostar/models/get_permissions_response_400.py +82 -0
  109. octostar/models/get_permissions_response_401.py +74 -0
  110. octostar/models/get_permissions_response_500.py +82 -0
  111. octostar/models/get_processing_status_response_200.py +104 -0
  112. octostar/models/get_processing_status_response_200_data.py +87 -0
  113. octostar/models/get_processing_status_response_400.py +82 -0
  114. octostar/models/get_processing_status_response_500.py +82 -0
  115. octostar/models/get_subscriptions_response_200_item.py +74 -0
  116. octostar/models/get_version_response_200.py +74 -0
  117. octostar/models/get_version_response_404.py +74 -0
  118. octostar/models/get_whoami_response_200.py +129 -0
  119. octostar/models/get_whoami_response_401.py +74 -0
  120. octostar/models/insert_entity.py +114 -0
  121. octostar/models/insert_entity_base.py +266 -0
  122. octostar/models/insert_entity_relationships_item.py +107 -0
  123. octostar/models/insert_entity_request.py +94 -0
  124. octostar/models/internal_server_error.py +82 -0
  125. octostar/models/job_execution_result.py +146 -0
  126. octostar/models/job_status.py +196 -0
  127. octostar/models/job_status_labels.py +60 -0
  128. octostar/models/job_with_url.py +82 -0
  129. octostar/models/kill_job_response_401.py +74 -0
  130. octostar/models/list_app_jobs_response_401.py +74 -0
  131. octostar/models/list_app_jobs_response_500.py +82 -0
  132. octostar/models/list_apps_response_401.py +74 -0
  133. octostar/models/list_apps_response_500.py +82 -0
  134. octostar/models/multi_query_json_body.py +100 -0
  135. octostar/models/multi_query_json_body_queries_item.py +80 -0
  136. octostar/models/multi_query_response_400.py +82 -0
  137. octostar/models/multi_query_response_401.py +74 -0
  138. octostar/models/not_found_error.py +74 -0
  139. octostar/models/octostar_event.py +96 -0
  140. octostar/models/octostar_event_octostar_payload.py +100 -0
  141. octostar/models/octostar_event_octostar_payload_level.py +11 -0
  142. octostar/models/os_notification.py +122 -0
  143. octostar/models/processing_status.py +262 -0
  144. octostar/models/processing_status_code.py +14 -0
  145. octostar/models/progress_request.py +73 -0
  146. octostar/models/publish_notification_response_401.py +74 -0
  147. octostar/models/pull_events_from_stream_response_401.py +74 -0
  148. octostar/models/push_event_to_stream_response_401.py +74 -0
  149. octostar/models/query_json_body.py +101 -0
  150. octostar/models/query_json_body_params.py +60 -0
  151. octostar/models/query_response_400.py +82 -0
  152. octostar/models/query_response_401.py +74 -0
  153. octostar/models/set_job_progress_response_401.py +74 -0
  154. octostar/models/string_to_value_label_map.py +99 -0
  155. octostar/models/string_to_value_label_map_data.py +89 -0
  156. octostar/models/string_to_value_label_map_data_additional_property.py +80 -0
  157. octostar/models/successful_get_tags.py +103 -0
  158. octostar/models/successful_insertion.py +98 -0
  159. octostar/models/tag_entities_response_401.py +74 -0
  160. octostar/models/toast_level.py +11 -0
  161. octostar/models/toast_response_401.py +74 -0
  162. octostar/models/undeploy_app_response_401.py +74 -0
  163. octostar/models/update_processing_status_response_200.py +82 -0
  164. octostar/models/update_processing_status_response_400.py +82 -0
  165. octostar/models/update_processing_status_response_500.py +82 -0
  166. octostar/models/upsert_entities_response_401.py +74 -0
  167. octostar/models/upsert_entity.py +114 -0
  168. octostar/models/upsert_entity_base.py +266 -0
  169. octostar/models/upsert_entity_relationships_item.py +107 -0
  170. octostar/py.typed +1 -0
  171. octostar/types.py +54 -0
  172. octostar/utils/__init__.py +15 -0
  173. octostar/utils/chat/__init__.py +0 -0
  174. octostar/utils/chat/chat.py +513 -0
  175. octostar/utils/chat/detokenize.py +105 -0
  176. octostar/utils/chat/get_default_model.py +50 -0
  177. octostar/utils/chat/list_models.py +91 -0
  178. octostar/utils/chat/tokenize.py +105 -0
  179. octostar/utils/commons.py +226 -0
  180. octostar/utils/exceptions.py +134 -0
  181. octostar/utils/jobs/__init__.py +0 -0
  182. octostar/utils/jobs/apps/__init__.py +0 -0
  183. octostar/utils/jobs/apps/deploy_app.py +81 -0
  184. octostar/utils/jobs/apps/execute_app_job.py +114 -0
  185. octostar/utils/jobs/apps/get_app_logs.py +113 -0
  186. octostar/utils/jobs/apps/get_app_secret.py +102 -0
  187. octostar/utils/jobs/apps/get_apps_url.py +73 -0
  188. octostar/utils/jobs/apps/list_app_jobs.py +62 -0
  189. octostar/utils/jobs/apps/list_apps.py +126 -0
  190. octostar/utils/jobs/apps/undeploy_app.py +48 -0
  191. octostar/utils/jobs/get_job_logs.py +113 -0
  192. octostar/utils/jobs/get_job_progress.py +76 -0
  193. octostar/utils/jobs/kill_job.py +47 -0
  194. octostar/utils/jobs/set_job_progress.py +67 -0
  195. octostar/utils/meta/__init__.py +0 -0
  196. octostar/utils/meta/get_version.py +30 -0
  197. octostar/utils/meta/get_whoami.py +30 -0
  198. octostar/utils/notifications/__init__.py +0 -0
  199. octostar/utils/notifications/delete_stream.py +58 -0
  200. octostar/utils/notifications/get_my_subscriptions.py +49 -0
  201. octostar/utils/notifications/publish_notification.py +73 -0
  202. octostar/utils/notifications/pull_event_from_stream.py +63 -0
  203. octostar/utils/notifications/pull_events_from_stream.py +64 -0
  204. octostar/utils/notifications/push_event_to_stream.py +109 -0
  205. octostar/utils/notifications/push_events_to_stream.py +137 -0
  206. octostar/utils/notifications/toast.py +92 -0
  207. octostar/utils/ontology/__init__.py +10 -0
  208. octostar/utils/ontology/fetch_ontology_data.py +141 -0
  209. octostar/utils/ontology/get_ontologies.py +55 -0
  210. octostar/utils/ontology/multiquery_ontology.py +287 -0
  211. octostar/utils/ontology/query_ontology.py +186 -0
  212. octostar/utils/pipeline/__init__.py +1 -0
  213. octostar/utils/pipeline/get_processing_status.py +230 -0
  214. octostar/utils/pipeline/update_processing_status.py +286 -0
  215. octostar/utils/search/__init__.py +11 -0
  216. octostar/utils/search/bulk_update.py +138 -0
  217. octostar/utils/search/count.py +117 -0
  218. octostar/utils/search/get_entity_annotations.py +304 -0
  219. octostar/utils/search/get_index_definition.py +111 -0
  220. octostar/utils/search/multi_search.py +129 -0
  221. octostar/utils/workspace/__init__.py +0 -0
  222. octostar/utils/workspace/delete_entities.py +247 -0
  223. octostar/utils/workspace/delete_entity.py +81 -0
  224. octostar/utils/workspace/delete_relationship.py +78 -0
  225. octostar/utils/workspace/delete_relationships.py +85 -0
  226. octostar/utils/workspace/delete_temporary_blob.py +85 -0
  227. octostar/utils/workspace/extract_entities.py +140 -0
  228. octostar/utils/workspace/get_filepath_from_item.py +85 -0
  229. octostar/utils/workspace/get_filepaths_from_items.py +100 -0
  230. octostar/utils/workspace/get_files_tree.py +102 -0
  231. octostar/utils/workspace/get_item_from_filepath.py +102 -0
  232. octostar/utils/workspace/get_items_from_filepaths.py +108 -0
  233. octostar/utils/workspace/linkcharts/__init__.py +0 -0
  234. octostar/utils/workspace/linkcharts/create_linkchart.py +241 -0
  235. octostar/utils/workspace/permissions/PermissionLevel.py +8 -0
  236. octostar/utils/workspace/permissions/__init__.py +1 -0
  237. octostar/utils/workspace/permissions/get_permissions.py +81 -0
  238. octostar/utils/workspace/read_attachment.py +284 -0
  239. octostar/utils/workspace/read_file.py +113 -0
  240. octostar/utils/workspace/read_temporary_blob.py +428 -0
  241. octostar/utils/workspace/saved_searches/__init__.py +0 -0
  242. octostar/utils/workspace/saved_searches/create_saved_search.py +183 -0
  243. octostar/utils/workspace/tags/__init__.py +0 -0
  244. octostar/utils/workspace/tags/delete_tag_from_entities.py +96 -0
  245. octostar/utils/workspace/tags/tag_entities.py +175 -0
  246. octostar/utils/workspace/upsert_entities.py +268 -0
  247. octostar/utils/workspace/upsert_entity.py +110 -0
  248. octostar/utils/workspace/upsert_relationship.py +128 -0
  249. octostar/utils/workspace/upsert_relationships.py +194 -0
  250. octostar/utils/workspace/write_attachment.py +263 -0
  251. octostar/utils/workspace/write_file.py +335 -0
  252. octostar/utils/workspace/write_temporary_blob.py +218 -0
  253. octostar_python_client-0.1.759.dist-info/METADATA +159 -0
  254. octostar_python_client-0.1.759.dist-info/RECORD +257 -0
  255. octostar_python_client-0.1.759.dist-info/WHEEL +5 -0
  256. octostar_python_client-0.1.759.dist-info/licenses/LICENSE +21 -0
  257. 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"]