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
@@ -0,0 +1,91 @@
1
+ import httpx
2
+ from typing import Optional
3
+ from ...client import Client, get_default_client
4
+ from ..exceptions import ApiConnectionError
5
+ from typing import Optional
6
+ from ..commons import network_retry_strategy
7
+
8
+
9
+ def _prepare_list_models_request(client):
10
+ endpoint_url = f"{client.get_base_url_v1()}/api/v1/chat/models"
11
+ headers = {
12
+ "Authorization": f"Bearer {client.token}",
13
+ "x-ontology": client.ontology,
14
+ }
15
+ return {
16
+ "url": endpoint_url,
17
+ "headers": headers,
18
+ "timeout": 60,
19
+ }
20
+
21
+
22
+ def sync(
23
+ client: Optional[Client] = None,
24
+ ) -> list[dict]:
25
+ """
26
+ # List the available AI chat models.
27
+
28
+ ## Arguments
29
+ - `client`: The Client with which to connect to Octostar. If None, the default one is used.
30
+
31
+ ## Returns
32
+ A list of information for each available AI model.
33
+
34
+ ## Raises
35
+ - `ApiConnectionError`: If the query was unsuccessful.
36
+ """
37
+ if not client:
38
+ client = get_default_client()
39
+ response = None
40
+ try:
41
+ for attempt in network_retry_strategy(retries=3):
42
+ with attempt:
43
+ with httpx.Client() as httpx_client:
44
+ response = httpx_client.get(**_prepare_list_models_request(client))
45
+ response.raise_for_status()
46
+ try:
47
+ response = response.json()
48
+ except Exception as e:
49
+ raise ConnectionError(
50
+ f"Unparsable response format {response.content} with exception {e}"
51
+ )
52
+ except Exception:
53
+ raise ApiConnectionError("list_models", response, client)
54
+ return response
55
+
56
+
57
+ async def asyncio(
58
+ client: Optional[Client] = None,
59
+ ) -> list[dict]:
60
+ """
61
+ # List asynchronously the available AI chat models.
62
+
63
+ ## Arguments
64
+ - `client`: The Client with which to connect to Octostar. If None, the default one is used.
65
+
66
+ ## Returns
67
+ A list of information for each available AI model.
68
+
69
+ ## Raises
70
+ - `ApiConnectionError`: If the query was unsuccessful.
71
+ """
72
+ if not client:
73
+ client = get_default_client()
74
+ response = None
75
+ try:
76
+ for attempt in network_retry_strategy(retries=3):
77
+ with attempt:
78
+ async with httpx.AsyncClient() as httpx_client:
79
+ response = await httpx_client.get(
80
+ **_prepare_list_models_request(client)
81
+ )
82
+ response.raise_for_status()
83
+ try:
84
+ response = response.json()
85
+ except Exception as e:
86
+ raise ConnectionError(
87
+ f"Unparsable response format {response.content} with exception {e}"
88
+ )
89
+ except Exception:
90
+ raise ApiConnectionError("list_models", response, client)
91
+ return response
@@ -0,0 +1,105 @@
1
+ import httpx
2
+ from typing import Optional
3
+ from ...client import Client, get_default_client
4
+ from ..exceptions import ApiConnectionError
5
+ from typing import Optional
6
+ from ..commons import network_retry_strategy
7
+
8
+
9
+ def _prepare_tokenize_request(client, text, model_name):
10
+ endpoint_url = f"{client.get_base_url_v1()}/api/v2/ai/llm/tokenize"
11
+ headers = {
12
+ "Authorization": f"Bearer {client.token}",
13
+ "x-ontology": client.ontology,
14
+ }
15
+ return {
16
+ "url": endpoint_url,
17
+ "headers": headers,
18
+ "timeout": 60,
19
+ "json": {
20
+ "text": text,
21
+ "model_name": model_name,
22
+ },
23
+ }
24
+
25
+
26
+ def sync(
27
+ text: str,
28
+ model_name: str,
29
+ client: Optional[Client] = None,
30
+ ) -> list[int]:
31
+ """
32
+ # Tokenize a string into a list of LLM tokens.
33
+
34
+ ## Arguments
35
+ - `text`: The input string to tokenize.
36
+ - `model_name`: The model with which to tokenize the input text.
37
+ - `client`: The Client with which to connect to Octostar. If None, the default one is used.
38
+
39
+ ## Returns
40
+ A list of tokens.
41
+
42
+ ## Raises
43
+ - `ApiConnectionError`: If the query was unsuccessful.
44
+ """
45
+ if not client:
46
+ client = get_default_client()
47
+ response = None
48
+ try:
49
+ for attempt in network_retry_strategy(retries=3):
50
+ with attempt:
51
+ with httpx.Client() as httpx_client:
52
+ response = httpx_client.post(
53
+ **_prepare_tokenize_request(client, text, model_name)
54
+ )
55
+ response.raise_for_status()
56
+ try:
57
+ response = response.json()["tokens"]
58
+ except Exception as e:
59
+ raise ConnectionError(
60
+ f"Unparsable response format {response.content} with exception {e}"
61
+ )
62
+ except Exception:
63
+ raise ApiConnectionError("tokenize", response, client)
64
+ return response # pyright: ignore[reportReturnType]
65
+
66
+
67
+ async def asyncio(
68
+ text: str,
69
+ model_name: str,
70
+ client: Optional[Client] = None,
71
+ ) -> list[int]:
72
+ """
73
+ # Tokenize a string into a list of LLM tokens.
74
+
75
+ ## Arguments
76
+ - `text`: The input string to tokenize.
77
+ - `model_name`: The model with which to tokenize the input text.
78
+ - `client`: The Client with which to connect to Octostar. If None, the default one is used.
79
+
80
+ ## Returns
81
+ A list of tokens.
82
+
83
+ ## Raises
84
+ - `ApiConnectionError`: If the query was unsuccessful.
85
+ """
86
+ if not client:
87
+ client = get_default_client()
88
+ response = None
89
+ try:
90
+ for attempt in network_retry_strategy(retries=3):
91
+ with attempt:
92
+ async with httpx.AsyncClient() as httpx_client:
93
+ response = await httpx_client.post(
94
+ **_prepare_tokenize_request(client, text, model_name)
95
+ )
96
+ response.raise_for_status()
97
+ try:
98
+ response = response.json()["tokens"]
99
+ except Exception as e:
100
+ raise ConnectionError(
101
+ f"Unparsable response format {response.content} with exception {e}"
102
+ )
103
+ except Exception:
104
+ raise ApiConnectionError("tokenize", response, client)
105
+ return response # pyright: ignore[reportReturnType]
@@ -0,0 +1,226 @@
1
+ import asyncio
2
+ from typing import Callable, Optional, List
3
+ import time
4
+ from tenacity import (
5
+ Retrying,
6
+ stop_after_attempt,
7
+ wait_exponential_jitter,
8
+ retry_if_exception,
9
+ )
10
+ import httpx
11
+ from calendar import timegm
12
+ from pytimeparse import parse as timeparse
13
+
14
+ from .exceptions import ApiConnectionError, StopAsyncIterationWithResult
15
+ from ..types import Response
16
+
17
+
18
+ TIMESTAMP_SKEW = 10
19
+
20
+ network_retry_strategy = lambda retries=3, retry_type=wait_exponential_jitter: Retrying(
21
+ stop=stop_after_attempt(retries),
22
+ wait=lambda state: network_wait_strategy(state, retry_type),
23
+ retry=retry_if_exception(network_retry_condition),
24
+ reraise=True,
25
+ )
26
+
27
+
28
+ def network_get_retry_after(exception):
29
+ if not isinstance(exception, ApiConnectionError):
30
+ return None
31
+ resp = getattr(exception, "response", None)
32
+ if not resp or not resp.headers:
33
+ return None
34
+ headers = resp.headers
35
+ headers = {k.lower(): v for k, v in resp.headers.items()}
36
+ wait_candidates = []
37
+
38
+ # Standard Retry-After header (RFC 7231)
39
+ retry_after = headers.get("retry-after")
40
+ if retry_after:
41
+ retry_after = retry_after.strip()
42
+ if retry_after.replace(".", "", 1).isdigit():
43
+ wait_candidates.append(float(retry_after))
44
+ else:
45
+ try:
46
+ retry_time = timegm(
47
+ time.strptime(retry_after, "%a, %d %b %Y %H:%M:%S GMT")
48
+ )
49
+ delta = retry_time - time.time()
50
+ if delta > 0:
51
+ wait_candidates.append(delta)
52
+ except Exception:
53
+ pass
54
+
55
+ # OpenAI / LiteLLM rate-limit reset headers
56
+ for key in ("x-ratelimit-reset-requests", "x-ratelimit-reset-tokens"):
57
+ val = headers.get(key)
58
+ if not val:
59
+ continue
60
+ val = val.strip()
61
+ if val.replace(".", "", 1).isdigit():
62
+ numeric = float(val)
63
+ now = time.time()
64
+ if numeric > now + TIMESTAMP_SKEW:
65
+ delta = numeric - now
66
+ if delta > 0:
67
+ wait_candidates.append(delta)
68
+ else:
69
+ if numeric > 0:
70
+ wait_candidates.append(numeric)
71
+ else:
72
+ try:
73
+ delta = timeparse(val)
74
+ if delta > 0:
75
+ wait_candidates.append(delta)
76
+ except Exception:
77
+ pass
78
+
79
+ if wait_candidates:
80
+ return max(wait_candidates)
81
+ return None
82
+
83
+
84
+ def network_retry_condition(exception):
85
+ if isinstance(exception, httpx.RequestError):
86
+ return True
87
+ if isinstance(exception, ApiConnectionError):
88
+ if hasattr(exception, "response") and exception.response:
89
+ sc = exception.response.status_code
90
+ return (300 <= sc <= 399) or (500 <= sc <= 599) or sc in [429, 408]
91
+ return True
92
+ if isinstance(exception, ConnectionError):
93
+ return True
94
+ return False
95
+
96
+
97
+ def network_wait_strategy(retry_state, default_retry_type=wait_exponential_jitter):
98
+ exception = retry_state.outcome.exception()
99
+ retry_after = network_get_retry_after(exception)
100
+ if retry_after is not None:
101
+ return retry_after
102
+ else:
103
+ return default_retry_type()(retry_state)
104
+
105
+
106
+ class FailCondition:
107
+ def __init__(self, callable: Callable[[Response], bool]):
108
+ self.callable = callable
109
+
110
+
111
+ class ChunksFailCondition:
112
+ def __init__(self, callable: Callable[[List[Response]], bool]):
113
+ self.callable = callable
114
+
115
+
116
+ class ChunksEndCondition:
117
+ def __init__(
118
+ self,
119
+ callable: Callable[[List[Response]], bool],
120
+ timeout: Optional[float] = None,
121
+ ):
122
+ self.callable = callable
123
+ self.timeout = timeout
124
+
125
+
126
+ class TimeoutAsyncGenerator:
127
+ def __init__(self, generator, timeout):
128
+ self.generator = generator
129
+ self.timeout = timeout
130
+ self.start_time = time.monotonic()
131
+
132
+ async def __aiter__(self):
133
+ gen_iter = self.generator.__aiter__()
134
+ while True:
135
+ elapsed_time = time.monotonic() - self.start_time
136
+ remaining_time = self.timeout - elapsed_time
137
+ if remaining_time <= 0:
138
+ raise asyncio.TimeoutError()
139
+ try:
140
+ item = await asyncio.wait_for(gen_iter.__anext__(), remaining_time)
141
+ yield item
142
+ except StopAsyncIteration:
143
+ break
144
+
145
+
146
+ def sync_request(
147
+ client,
148
+ module,
149
+ fail_condition=None,
150
+ retry_strategy=network_retry_strategy,
151
+ **request_kwargs,
152
+ ):
153
+ if not fail_condition:
154
+ fail_condition = FailCondition(callable=lambda resp: resp.status_code != 200)
155
+ for attempt in retry_strategy():
156
+ with attempt:
157
+ response = module.sync_detailed(**request_kwargs, client=client)
158
+ if fail_condition.callable(response):
159
+ raise ApiConnectionError(module.__name__, response, client)
160
+ return response
161
+
162
+
163
+ async def asyncio_request(
164
+ client,
165
+ module,
166
+ fail_condition=None,
167
+ retry_strategy=network_retry_strategy,
168
+ **request_kwargs,
169
+ ):
170
+ if not fail_condition:
171
+ fail_condition = FailCondition(callable=lambda resp: resp.status_code != 200)
172
+ for attempt in retry_strategy():
173
+ with attempt:
174
+ response = await module.asyncio_detailed(**request_kwargs, client=client)
175
+ if fail_condition.callable(response):
176
+ raise ApiConnectionError(module.__name__, response, client)
177
+ return response
178
+
179
+
180
+ async def streaming_request(
181
+ client,
182
+ module,
183
+ fail_condition=None,
184
+ end_condition=None,
185
+ retry_strategy=network_retry_strategy,
186
+ **request_kwargs,
187
+ ):
188
+ async def _collect_chunks(
189
+ generator, collected_chunks, fail_condition, end_condition
190
+ ):
191
+ if end_condition.timeout:
192
+ generator = TimeoutAsyncGenerator(generator, end_condition.timeout)
193
+ try:
194
+ async for chunk in generator:
195
+ if fail_condition.callable(collected_chunks + [chunk]):
196
+ raise ApiConnectionError(module.__name__, chunk, client)
197
+ else:
198
+ collected_chunks.append(chunk)
199
+ if end_condition.callable(collected_chunks):
200
+ raise StopAsyncIterationWithResult(value=collected_chunks)
201
+ else:
202
+ yield chunk
203
+ except asyncio.TimeoutError:
204
+ raise StopAsyncIterationWithResult(value=collected_chunks)
205
+
206
+ if not fail_condition:
207
+ fail_condition = ChunksFailCondition(
208
+ callable=lambda resps: not resps or resps[0].status_code != 200
209
+ )
210
+ if not end_condition:
211
+ end_condition = ChunksEndCondition(callable=lambda resps: False)
212
+ collected_chunks = []
213
+ for attempt in retry_strategy():
214
+ with attempt:
215
+ generator = module.streaming_detailed(**request_kwargs, client=client)
216
+ generator = _collect_chunks(
217
+ generator, collected_chunks, fail_condition, end_condition
218
+ )
219
+ async for chunk in generator:
220
+ yield chunk
221
+ raise StopAsyncIterationWithResult(value=collected_chunks)
222
+
223
+
224
+ def streaming_get_full_content(responses):
225
+ chunks = [part.content for part in responses if part.content.strip()]
226
+ return b"".join(chunks)
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any
5
+
6
+
7
+ class StopAsyncIterationWithResult(Exception):
8
+ def __init__(self, value):
9
+ self.value = value
10
+
11
+
12
+ class ApiConnectionError(ConnectionError):
13
+ def __init__(self, method_name, response, client, max_message_length=10000):
14
+ self.method_name = method_name
15
+ self.response = response
16
+ self.client = client
17
+
18
+ message = (
19
+ "{method_name} failed with status code {code} and response\n{response}"
20
+ )
21
+ status_code = getattr(response, "status_code", "Unknown")
22
+ content = getattr(response, "content", "No content available")
23
+ message = message.format(
24
+ method_name=method_name, code=status_code, response=content
25
+ )
26
+ if len(message) > max_message_length:
27
+ message = message[: max_message_length - 3] + "..."
28
+ super().__init__(message)
29
+
30
+
31
+ # FIXME: TEAM-6154 — drop this regex once the server returns the
32
+ # rejected field as a structured attribute on EntityError. Today
33
+ # UpsertResponse carries each rejection only as a free-form
34
+ # ``error: str``, so we extract the field name client-side to
35
+ # populate ``invalid_fields``.
36
+ #
37
+ # Matches per-field server validation rejections inside an entity's error text:
38
+ # "Invalid <type> value for field '<name>'[: '<value>']"
39
+ _FIELD_FAILURE_RE = re.compile(
40
+ r"Invalid\s+\S+\s+value\s+for\s+field\s+'(?P<field>[^']+)'",
41
+ re.IGNORECASE,
42
+ )
43
+
44
+ # FIXME: TEAM-6154 — drop this regex too, same task as
45
+ # _FIELD_FAILURE_RE above. The server should expose the rejected
46
+ # reserved-keyword token as a structured attribute on EntityError.
47
+ #
48
+ # Matches "reserved keyword" mentions inside an entity's error text, capturing
49
+ # the offending token when it appears quoted (e.g. ``reserved keyword 'call'``).
50
+ # Some backends omit the quotes, so callers should also fall back to
51
+ # ``"reserved keyword" in err["error"]`` for boolean detection.
52
+ _RESERVED_KEYWORD_RE = re.compile(
53
+ r"reserved\s+keyword[^'\"]*['\"](?P<keyword>[^'\"]+)['\"]",
54
+ re.IGNORECASE,
55
+ )
56
+
57
+
58
+ class ApiValidationError(ApiConnectionError):
59
+ """Raised when the server accepted the upsert request but rejected one
60
+ or more entities server-side.
61
+
62
+ Subclass of :class:`ApiConnectionError`, so handlers catching the
63
+ parent class still receive validation failures.
64
+
65
+ The per-entity breakdown is exposed via structured attributes:
66
+
67
+ Attributes:
68
+ entity_errors: ``list[dict]`` — ``[{"entity_id": str, "error":
69
+ str}, ...]``, the per-entity error rows as returned by the
70
+ server. Use this when you need the raw message text for
71
+ each failing entity.
72
+ missing_entity_ids: ``set[str]`` — IDs that were sent but appear
73
+ in neither ``response.entities`` nor ``response.errors``.
74
+ The server has no record of these: neither written nor
75
+ explicitly rejected.
76
+ mismatched_keys: ``list[tuple[str, str]]`` — ``(entity_id,
77
+ os_workspace)`` pairs the server returned under an
78
+ ``os_workspace`` different from the one you sent. Typically
79
+ indicates a stale record was matched; refetching is the
80
+ usual recovery.
81
+ invalid_fields: ``dict[str, list[str]]`` — ``{entity_id:
82
+ [field_name, ...]}`` covering per-field rejections
83
+ (malformed datetime, bad number, etc.). To recover, drop
84
+ the listed fields from the payload for each entity and
85
+ re-upsert.
86
+ reserved_keyword_entity_types: ``set[str]`` — entity-type
87
+ tokens the server's query parser flagged as reserved
88
+ keywords (e.g. Cypher ``call``, ``match``). Resolution is
89
+ usually renaming the entity type at the source ontology.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ method_name,
95
+ response,
96
+ client,
97
+ *,
98
+ entity_errors: list[dict[str, Any]] | None = None,
99
+ missing_entity_ids: set[str] | None = None,
100
+ mismatched_keys: list[tuple[str, str]] | None = None,
101
+ max_message_length: int = 10000,
102
+ ):
103
+ self.entity_errors: list[dict[str, Any]] = list(entity_errors or [])
104
+ self.missing_entity_ids: set[str] = set(missing_entity_ids or ())
105
+ self.mismatched_keys: list[tuple[str, str]] = list(mismatched_keys or ())
106
+ super().__init__(
107
+ method_name, response, client, max_message_length=max_message_length
108
+ )
109
+
110
+ @property
111
+ def invalid_fields(self) -> dict[str, list[str]]:
112
+ """Per-entity invalid field names parsed from the server's error strings."""
113
+ result: dict[str, list[str]] = {}
114
+ for err in self.entity_errors:
115
+ entity_id = err.get("entity_id")
116
+ error_text = err.get("error") or ""
117
+ if not entity_id:
118
+ continue
119
+ for m in _FIELD_FAILURE_RE.finditer(error_text):
120
+ bucket = result.setdefault(entity_id, [])
121
+ field = m.group("field")
122
+ if field not in bucket:
123
+ bucket.append(field)
124
+ return result
125
+
126
+ @property
127
+ def reserved_keyword_entity_types(self) -> set[str]:
128
+ """Entity-type tokens extracted from quoted "reserved keyword '<x>'" mentions."""
129
+ result: set[str] = set()
130
+ for err in self.entity_errors:
131
+ error_text = err.get("error") or ""
132
+ for m in _RESERVED_KEYWORD_RE.finditer(error_text):
133
+ result.add(m.group("keyword"))
134
+ return result
File without changes
File without changes
@@ -0,0 +1,81 @@
1
+ import os
2
+ import logging
3
+ from typing import Any, Optional
4
+
5
+ _logger = logging.getLogger(__name__)
6
+
7
+ from ....api.apps import deploy_app
8
+ from ....models.deploy_app_json_body import DeployAppJsonBody
9
+ from ....models.deploy_app_json_body_secrets import DeployAppJsonBodySecrets
10
+ from ....client import Client
11
+ from ...exceptions import ApiConnectionError
12
+
13
+
14
+ def sync(
15
+ workspace_id: str,
16
+ os_entity_uid: str,
17
+ secrets: Optional[dict[str, Any]] = None,
18
+ client: Client = None,
19
+ ):
20
+ """
21
+ # Deploys the App defined by a given workspace and entity (the app root folder).
22
+
23
+ ## Arguments
24
+ - `workspace_id`: The ID of the workspace containing the app folder.
25
+ - `os_entity_uid`: The entity ID of the app folder.
26
+ - `secrets`: An optional key-value pair of secrets to deploy the app with.
27
+ - `client`: The Client with which to connect to Octostar. If None, the default one is used.
28
+
29
+ ## Returns
30
+ The app entity.
31
+
32
+ ## Raises
33
+ - `ApiConnectionError`: If the request was unsuccessful on the server.
34
+ """
35
+ if not secrets:
36
+ secrets = {}
37
+ secrets_body = DeployAppJsonBody(DeployAppJsonBodySecrets(**secrets))
38
+ response = deploy_app.sync_detailed(
39
+ workspace=workspace_id,
40
+ os_entity_uid=os_entity_uid,
41
+ json_body=secrets_body,
42
+ client=client,
43
+ )
44
+ if response.status_code != 200:
45
+ raise ApiConnectionError("deploy_app", response, client)
46
+ return response.parsed
47
+
48
+
49
+ async def asyncio(
50
+ workspace_id: str,
51
+ os_entity_uid: str,
52
+ secrets: Optional[dict[str, Any]] = None,
53
+ client: Client = None,
54
+ ):
55
+ """
56
+ # Deploys asynchronously the App defined by a given workspace and entity (the app root folder).
57
+
58
+ ## Arguments
59
+ - `workspace_id`: The ID of the workspace containing the app folder.
60
+ - `os_entity_uid`: The entity ID of the app folder.
61
+ - `secrets`: An optional key-value pair of secrets to deploy the app with.
62
+ - `client`: The Client with which to connect to Octostar. If None, the default one is used.
63
+
64
+ ## Returns
65
+ The app entity.
66
+
67
+ ## Raises
68
+ - `ApiConnectionError`: If the request was unsuccessful on the server.
69
+ """
70
+ if not secrets:
71
+ secrets = {}
72
+ secrets_body = DeployAppJsonBody(DeployAppJsonBodySecrets(**secrets))
73
+ response = await deploy_app.asyncio_detailed(
74
+ workspace=workspace_id,
75
+ os_entity_uid=os_entity_uid,
76
+ json_body=secrets_body,
77
+ client=client,
78
+ )
79
+ if response.status_code != 200:
80
+ raise ApiConnectionError("deploy_app", response, client)
81
+ return response.parsed