acceldata-sdk-python 26.7.0__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 (303) hide show
  1. acceldata/__init__.py +7 -0
  2. acceldata/client/__init__.py +0 -0
  3. acceldata/client/adoc_client.py +666 -0
  4. acceldata/client/http_client.py +346 -0
  5. acceldata/client/transient_retry.py +142 -0
  6. acceldata/constants/__init__.py +1 -0
  7. acceldata/constants/constants.py +13 -0
  8. acceldata/exceptions/__init__.py +52 -0
  9. acceldata/exceptions/api_errors.py +85 -0
  10. acceldata/exceptions/exceptions.py +174 -0
  11. acceldata/exceptions/sdk_exceptions.py +98 -0
  12. acceldata/mappers/__init__.py +21 -0
  13. acceldata/mappers/catalog_executions.py +107 -0
  14. acceldata/models/__init__.py +0 -0
  15. acceldata/models/api/catalog/above_configs.py +99 -0
  16. acceldata/models/api/catalog/above_configs_success.py +138 -0
  17. acceldata/models/api/catalog/absolute_configs.py +197 -0
  18. acceldata/models/api/catalog/absolute_configs_fallback.py +101 -0
  19. acceldata/models/api/catalog/absolute_threshold_config.py +105 -0
  20. acceldata/models/api/catalog/analysis_job_context.py +102 -0
  21. acceldata/models/api/catalog/analytics_pipeline.py +385 -0
  22. acceldata/models/api/catalog/analytics_pipeline_endpoint.py +136 -0
  23. acceldata/models/api/catalog/anomaly_config.py +95 -0
  24. acceldata/models/api/catalog/anomaly_details.py +119 -0
  25. acceldata/models/api/catalog/anomaly_threshold_config.py +105 -0
  26. acceldata/models/api/catalog/artifact.py +92 -0
  27. acceldata/models/api/catalog/assembly.py +171 -0
  28. acceldata/models/api/catalog/asset.py +162 -0
  29. acceldata/models/api/catalog/asset_catalog_listing.py +159 -0
  30. acceldata/models/api/catalog/asset_catalog_listing_response.py +102 -0
  31. acceldata/models/api/catalog/asset_label_request.py +96 -0
  32. acceldata/models/api/catalog/asset_progress.py +96 -0
  33. acceldata/models/api/catalog/asset_progress_details.py +117 -0
  34. acceldata/models/api/catalog/asset_tag.py +124 -0
  35. acceldata/models/api/catalog/asset_tag_info.py +140 -0
  36. acceldata/models/api/catalog/asset_tag_request.py +95 -0
  37. acceldata/models/api/catalog/asset_tag_response.py +96 -0
  38. acceldata/models/api/catalog/asset_tags_response.py +102 -0
  39. acceldata/models/api/catalog/asset_type.py +96 -0
  40. acceldata/models/api/catalog/auto_anomaly_feature_analysis.py +160 -0
  41. acceldata/models/api/catalog/auto_anomaly_item_execution.py +133 -0
  42. acceldata/models/api/catalog/auto_anomaly_meta_data.py +162 -0
  43. acceldata/models/api/catalog/auto_anomaly_rule_execution.py +102 -0
  44. acceldata/models/api/catalog/auto_anomaly_summary.py +124 -0
  45. acceldata/models/api/catalog/backing_asset.py +126 -0
  46. acceldata/models/api/catalog/below_configs.py +99 -0
  47. acceldata/models/api/catalog/big_decimal.py +100 -0
  48. acceldata/models/api/catalog/big_integer.py +98 -0
  49. acceldata/models/api/catalog/bounds_date_time_marker_config.py +110 -0
  50. acceldata/models/api/catalog/bounds_file_event_marker_config.py +108 -0
  51. acceldata/models/api/catalog/bounds_id_marker_config.py +96 -0
  52. acceldata/models/api/catalog/catalog_asset_meta_data.py +128 -0
  53. acceldata/models/api/catalog/changes_by_config.py +105 -0
  54. acceldata/models/api/catalog/column_mapping.py +139 -0
  55. acceldata/models/api/catalog/column_metadata.py +90 -0
  56. acceldata/models/api/catalog/column_metric_with_anomaly_details.py +119 -0
  57. acceldata/models/api/catalog/column_variable.py +111 -0
  58. acceldata/models/api/catalog/composite_policy_execution.py +102 -0
  59. acceldata/models/api/catalog/composite_policy_item_execution.py +104 -0
  60. acceldata/models/api/catalog/connection.py +118 -0
  61. acceldata/models/api/catalog/connection_test_result.py +106 -0
  62. acceldata/models/api/catalog/connection_type.py +90 -0
  63. acceldata/models/api/catalog/connection_types_response.py +96 -0
  64. acceldata/models/api/catalog/crawler.py +88 -0
  65. acceldata/models/api/catalog/crawler_action_response.py +90 -0
  66. acceldata/models/api/catalog/crawler_execution.py +147 -0
  67. acceldata/models/api/catalog/crawler_execution_response.py +102 -0
  68. acceldata/models/api/catalog/crawler_status_response.py +90 -0
  69. acceldata/models/api/catalog/create_profile.py +194 -0
  70. acceldata/models/api/catalog/custom_sql_config.py +121 -0
  71. acceldata/models/api/catalog/data_bricks_spark_resource_config.py +104 -0
  72. acceldata/models/api/catalog/data_cadence_rule.py +236 -0
  73. acceldata/models/api/catalog/data_cadence_rule_details.py +125 -0
  74. acceldata/models/api/catalog/data_cadence_rule_execution.py +102 -0
  75. acceldata/models/api/catalog/data_cadence_rule_item.py +104 -0
  76. acceldata/models/api/catalog/data_cadence_rule_item_execution.py +158 -0
  77. acceldata/models/api/catalog/data_cadence_rule_response.py +98 -0
  78. acceldata/models/api/catalog/data_drift_execution.py +102 -0
  79. acceldata/models/api/catalog/data_drift_item_execution.py +121 -0
  80. acceldata/models/api/catalog/data_quality_engine.py +90 -0
  81. acceldata/models/api/catalog/data_quality_execution.py +106 -0
  82. acceldata/models/api/catalog/data_quality_rule.py +323 -0
  83. acceldata/models/api/catalog/data_quality_rule_details.py +186 -0
  84. acceldata/models/api/catalog/data_quality_rule_response.py +120 -0
  85. acceldata/models/api/catalog/data_quality_rules_response.py +102 -0
  86. acceldata/models/api/catalog/databricks_compute_config.py +113 -0
  87. acceldata/models/api/catalog/dataplane_engine.py +105 -0
  88. acceldata/models/api/catalog/dataplane_health_metrics.py +101 -0
  89. acceldata/models/api/catalog/dataplane_job.py +216 -0
  90. acceldata/models/api/catalog/dataplane_job_status.py +113 -0
  91. acceldata/models/api/catalog/dataplane_request.py +96 -0
  92. acceldata/models/api/catalog/dataproc_compute_config.py +106 -0
  93. acceldata/models/api/catalog/dataproc_spark_resource_config.py +104 -0
  94. acceldata/models/api/catalog/date_based_partition_marker_config.py +116 -0
  95. acceldata/models/api/catalog/date_partitioning_marker_config.py +113 -0
  96. acceldata/models/api/catalog/date_time_marker_config.py +109 -0
  97. acceldata/models/api/catalog/day_month_year_based_partition_marker_config.py +124 -0
  98. acceldata/models/api/catalog/decreased_by_config.py +105 -0
  99. acceldata/models/api/catalog/delete_artifact_response.py +88 -0
  100. acceldata/models/api/catalog/dq_rule_execution.py +248 -0
  101. acceldata/models/api/catalog/drift.py +121 -0
  102. acceldata/models/api/catalog/enable_rule_request.py +88 -0
  103. acceldata/models/api/catalog/engine.py +90 -0
  104. acceldata/models/api/catalog/execution_cancellation_response.py +92 -0
  105. acceldata/models/api/catalog/execution_response.py +144 -0
  106. acceldata/models/api/catalog/execution_result.py +126 -0
  107. acceldata/models/api/catalog/executions_response.py +102 -0
  108. acceldata/models/api/catalog/failed_asset_detail.py +90 -0
  109. acceldata/models/api/catalog/failed_summary.py +98 -0
  110. acceldata/models/api/catalog/file.py +94 -0
  111. acceldata/models/api/catalog/file_marker_config.py +99 -0
  112. acceldata/models/api/catalog/generic_config_fallback.py +101 -0
  113. acceldata/models/api/catalog/generic_execution.py +221 -0
  114. acceldata/models/api/catalog/generic_rule_execution.py +137 -0
  115. acceldata/models/api/catalog/global_storage_config_dto.py +172 -0
  116. acceldata/models/api/catalog/hosted_dp_deployment_response.py +98 -0
  117. acceldata/models/api/catalog/id_marker_config.py +94 -0
  118. acceldata/models/api/catalog/increased_by_config.py +105 -0
  119. acceldata/models/api/catalog/item.py +160 -0
  120. acceldata/models/api/catalog/item_execution.py +192 -0
  121. acceldata/models/api/catalog/item_segment.py +131 -0
  122. acceldata/models/api/catalog/job_schedule.py +118 -0
  123. acceldata/models/api/catalog/job_spec.py +96 -0
  124. acceldata/models/api/catalog/kubernetes_metrics.py +91 -0
  125. acceldata/models/api/catalog/label.py +97 -0
  126. acceldata/models/api/catalog/livy_compute_config.py +103 -0
  127. acceldata/models/api/catalog/livy_spark_resource_config.py +106 -0
  128. acceldata/models/api/catalog/look_back_config.py +90 -0
  129. acceldata/models/api/catalog/manual_tag_request.py +92 -0
  130. acceldata/models/api/catalog/mapping.py +97 -0
  131. acceldata/models/api/catalog/marker.py +88 -0
  132. acceldata/models/api/catalog/marker_config.py +349 -0
  133. acceldata/models/api/catalog/marker_config_fallback.py +101 -0
  134. acceldata/models/api/catalog/meta.py +92 -0
  135. acceldata/models/api/catalog/meta_data.py +92 -0
  136. acceldata/models/api/catalog/meta_data_item.py +94 -0
  137. acceldata/models/api/catalog/mini_profile_request.py +112 -0
  138. acceldata/models/api/catalog/notification_payload.py +114 -0
  139. acceldata/models/api/catalog/offset_based_marker_config.py +97 -0
  140. acceldata/models/api/catalog/operational_config.py +92 -0
  141. acceldata/models/api/catalog/pattern_configuration.py +90 -0
  142. acceldata/models/api/catalog/pipe_line_request.py +92 -0
  143. acceldata/models/api/catalog/pipeline_compute_config.py +110 -0
  144. acceldata/models/api/catalog/policy_execution_marker_config.py +94 -0
  145. acceldata/models/api/catalog/policy_execution_request.py +229 -0
  146. acceldata/models/api/catalog/policy_group.py +114 -0
  147. acceldata/models/api/catalog/policy_transform_udf.py +113 -0
  148. acceldata/models/api/catalog/process_metrics.py +91 -0
  149. acceldata/models/api/catalog/profile_anomaly_details_response.py +109 -0
  150. acceldata/models/api/catalog/profile_anomaly_item_execution.py +127 -0
  151. acceldata/models/api/catalog/profile_anomaly_rule_execution.py +102 -0
  152. acceldata/models/api/catalog/profile_anomaly_rule_item.py +109 -0
  153. acceldata/models/api/catalog/profile_cancellation_response.py +92 -0
  154. acceldata/models/api/catalog/profile_request.py +211 -0
  155. acceldata/models/api/catalog/profile_request_response.py +92 -0
  156. acceldata/models/api/catalog/property_template.py +178 -0
  157. acceldata/models/api/catalog/property_templates.py +123 -0
  158. acceldata/models/api/catalog/pub_sub_lookback_marker_config.py +96 -0
  159. acceldata/models/api/catalog/pub_sub_timestamp_marker_config.py +108 -0
  160. acceldata/models/api/catalog/quality_rule.py +381 -0
  161. acceldata/models/api/catalog/quality_rule_execution_metrics.py +114 -0
  162. acceldata/models/api/catalog/quality_rule_with_latest_execution.py +115 -0
  163. acceldata/models/api/catalog/range_configs.py +109 -0
  164. acceldata/models/api/catalog/reconciliation_item.py +102 -0
  165. acceldata/models/api/catalog/reconciliation_item_execution.py +157 -0
  166. acceldata/models/api/catalog/reconciliation_rule.py +339 -0
  167. acceldata/models/api/catalog/reconciliation_rule_details.py +225 -0
  168. acceldata/models/api/catalog/reconciliation_rule_execution.py +102 -0
  169. acceldata/models/api/catalog/reconciliation_rule_response.py +98 -0
  170. acceldata/models/api/catalog/reconciliation_rules_response.py +102 -0
  171. acceldata/models/api/catalog/relative_config.py +197 -0
  172. acceldata/models/api/catalog/relative_config_fallback.py +101 -0
  173. acceldata/models/api/catalog/relative_threshold_config.py +111 -0
  174. acceldata/models/api/catalog/root_cause_analysis.py +108 -0
  175. acceldata/models/api/catalog/rule_execution.py +327 -0
  176. acceldata/models/api/catalog/rule_execution_result.py +90 -0
  177. acceldata/models/api/catalog/rule_item_result.py +125 -0
  178. acceldata/models/api/catalog/rule_meta_data.py +94 -0
  179. acceldata/models/api/catalog/rule_spark_sql_dynamic_filter_variable_mapping.py +100 -0
  180. acceldata/models/api/catalog/rule_tag.py +101 -0
  181. acceldata/models/api/catalog/rule_threshold_configuration.py +236 -0
  182. acceldata/models/api/catalog/rule_threshold_configuration_fallback.py +101 -0
  183. acceldata/models/api/catalog/rules_with_latest_execution_response.py +102 -0
  184. acceldata/models/api/catalog/sample_data_api_response.py +122 -0
  185. acceldata/models/api/catalog/schema_drift_item_execution.py +131 -0
  186. acceldata/models/api/catalog/schema_drift_rule_execution.py +102 -0
  187. acceldata/models/api/catalog/secret_manager_configuration.py +90 -0
  188. acceldata/models/api/catalog/secret_manager_configuration_with_option.py +98 -0
  189. acceldata/models/api/catalog/security_config.py +90 -0
  190. acceldata/models/api/catalog/segment_metric_json.py +142 -0
  191. acceldata/models/api/catalog/segments_analysis.py +145 -0
  192. acceldata/models/api/catalog/short_segment.py +97 -0
  193. acceldata/models/api/catalog/simple_quality_rule_response.py +92 -0
  194. acceldata/models/api/catalog/snapshot_data.py +117 -0
  195. acceldata/models/api/catalog/source_model.py +90 -0
  196. acceldata/models/api/catalog/source_type.py +100 -0
  197. acceldata/models/api/catalog/source_types_response.py +102 -0
  198. acceldata/models/api/catalog/spark_job_status.py +113 -0
  199. acceldata/models/api/catalog/spark_resource_config.py +114 -0
  200. acceldata/models/api/catalog/standard_response_assembly.py +92 -0
  201. acceldata/models/api/catalog/standard_response_crawler_action_response.py +92 -0
  202. acceldata/models/api/catalog/standard_response_crawler_status_response.py +92 -0
  203. acceldata/models/api/catalog/standard_response_delete_artifact_response.py +92 -0
  204. acceldata/models/api/catalog/standard_response_list.py +96 -0
  205. acceldata/models/api/catalog/standard_response_map.py +88 -0
  206. acceldata/models/api/catalog/standard_response_meta_data.py +92 -0
  207. acceldata/models/api/catalog/standard_response_mini_profile_request.py +92 -0
  208. acceldata/models/api/catalog/standard_response_profile_request.py +92 -0
  209. acceldata/models/api/catalog/start_profiling_request.py +92 -0
  210. acceldata/models/api/catalog/tag_domain.py +116 -0
  211. acceldata/models/api/catalog/threshold_breach_details.py +101 -0
  212. acceldata/models/api/catalog/time_based_config.py +92 -0
  213. acceldata/models/api/catalog/time_based_threshold_config.py +105 -0
  214. acceldata/models/api/catalog/timestamp_based_marker_config.py +115 -0
  215. acceldata/models/api/catalog/udf_variable.py +97 -0
  216. acceldata/models/api/catalog/yunikorn_compute_config.py +133 -0
  217. acceldata/models/api/catalog/yunikorn_spark_resource_config.py +154 -0
  218. acceldata/models/api/pipeline/big_decimal.py +100 -0
  219. acceldata/models/api/pipeline/big_integer.py +98 -0
  220. acceldata/models/api/pipeline/child_span_response.py +96 -0
  221. acceldata/models/api/pipeline/create_span_event.py +109 -0
  222. acceldata/models/api/pipeline/edge.py +108 -0
  223. acceldata/models/api/pipeline/event_alert_object.py +95 -0
  224. acceldata/models/api/pipeline/generic_config_fallback.py +101 -0
  225. acceldata/models/api/pipeline/identifier.py +146 -0
  226. acceldata/models/api/pipeline/job_request.py +130 -0
  227. acceldata/models/api/pipeline/meta.py +107 -0
  228. acceldata/models/api/pipeline/meta_data.py +92 -0
  229. acceldata/models/api/pipeline/metric.py +103 -0
  230. acceldata/models/api/pipeline/node.py +201 -0
  231. acceldata/models/api/pipeline/node_response.py +92 -0
  232. acceldata/models/api/pipeline/notification_payload.py +114 -0
  233. acceldata/models/api/pipeline/pipeline.py +152 -0
  234. acceldata/models/api/pipeline/pipeline_baseline_metric.py +92 -0
  235. acceldata/models/api/pipeline/pipeline_details.py +118 -0
  236. acceldata/models/api/pipeline/pipeline_details_response.py +102 -0
  237. acceldata/models/api/pipeline/pipeline_input_request.py +92 -0
  238. acceldata/models/api/pipeline/pipeline_listing_info.py +118 -0
  239. acceldata/models/api/pipeline/pipeline_response.py +92 -0
  240. acceldata/models/api/pipeline/pipeline_run.py +136 -0
  241. acceldata/models/api/pipeline/pipeline_run_request.py +92 -0
  242. acceldata/models/api/pipeline/pipeline_runs_response.py +102 -0
  243. acceldata/models/api/pipeline/pipeline_summary.py +98 -0
  244. acceldata/models/api/pipeline/pipelines_listing_response.py +102 -0
  245. acceldata/models/api/pipeline/policy_summary.py +102 -0
  246. acceldata/models/api/pipeline/span.py +115 -0
  247. acceldata/models/api/pipeline/span_event.py +114 -0
  248. acceldata/models/api/pipeline/span_event_request.py +92 -0
  249. acceldata/models/api/pipeline/span_event_response.py +92 -0
  250. acceldata/models/api/pipeline/span_request.py +94 -0
  251. acceldata/models/api/pipeline/spans_response.py +96 -0
  252. acceldata/models/api/pipeline/tag.py +97 -0
  253. acceldata/models/sdk/__init__.py +0 -0
  254. acceldata/models/sdk/catalog/__init__.py +105 -0
  255. acceldata/models/sdk/catalog/asset_resource.py +112 -0
  256. acceldata/models/sdk/catalog/asset_source_type.py +59 -0
  257. acceldata/models/sdk/catalog/custom_asset_metadata.py +11 -0
  258. acceldata/models/sdk/catalog/datasource_resource.py +88 -0
  259. acceldata/models/sdk/catalog/executor.py +524 -0
  260. acceldata/models/sdk/catalog/policy_execution_request.py +114 -0
  261. acceldata/models/sdk/catalog/policy_execution_type.py +16 -0
  262. acceldata/models/sdk/catalog/policy_filter.py +94 -0
  263. acceldata/models/sdk/catalog/policy_rule_resources.py +388 -0
  264. acceldata/models/sdk/catalog/profile_request_resource.py +77 -0
  265. acceldata/models/sdk/catalog/profiling.py +82 -0
  266. acceldata/models/sdk/catalog/profiling_configuration_error.py +5 -0
  267. acceldata/models/sdk/pipeline/__init__.py +62 -0
  268. acceldata/models/sdk/pipeline/create_job_input.py +108 -0
  269. acceldata/models/sdk/pipeline/create_pipeline_input_request.py +72 -0
  270. acceldata/models/sdk/pipeline/events/__init__.py +5 -0
  271. acceldata/models/sdk/pipeline/events/generic_event.py +31 -0
  272. acceldata/models/sdk/pipeline/events/log_event.py +34 -0
  273. acceldata/models/sdk/pipeline/events/span_event.py +8 -0
  274. acceldata/models/sdk/pipeline/pipeline_resource.py +171 -0
  275. acceldata/models/sdk/pipeline/pipeline_run_input.py +84 -0
  276. acceldata/models/sdk/pipeline/pipeline_run_resource.py +208 -0
  277. acceldata/models/sdk/pipeline/pipeline_source_type.py +20 -0
  278. acceldata/models/sdk/pipeline/span_resource.py +327 -0
  279. acceldata/openapi_deserialization_patches.py +161 -0
  280. acceldata/openapi_repr_patches.py +62 -0
  281. acceldata/services/__init__.py +0 -0
  282. acceldata/services/api_timestamp.py +31 -0
  283. acceldata/services/asset_service.py +651 -0
  284. acceldata/services/datasource_service.py +120 -0
  285. acceldata/services/generic_service.py +132 -0
  286. acceldata/services/marker_config.py +232 -0
  287. acceldata/services/pipeline_big_decimal_api.py +102 -0
  288. acceldata/services/pipeline_response_parse.py +209 -0
  289. acceldata/services/pipeline_service.py +494 -0
  290. acceldata/services/rule_service.py +620 -0
  291. acceldata/urls/__init__.py +1 -0
  292. acceldata/urls/catalog_urls.py +137 -0
  293. acceldata/urls/pipeline_urls.py +55 -0
  294. acceldata/utils/__init__.py +6 -0
  295. acceldata/utils/datetime_utils.py +16 -0
  296. acceldata/utils/operation_ack.py +22 -0
  297. acceldata/utils/span_helpers.py +95 -0
  298. acceldata/utils/time_range_utils.py +90 -0
  299. acceldata_sdk_python-26.7.0.dist-info/METADATA +26 -0
  300. acceldata_sdk_python-26.7.0.dist-info/RECORD +303 -0
  301. acceldata_sdk_python-26.7.0.dist-info/WHEEL +5 -0
  302. acceldata_sdk_python-26.7.0.dist-info/licenses/LICENSE +22 -0
  303. acceldata_sdk_python-26.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,651 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from typing import Any, Mapping, Optional
6
+
7
+ from pydantic import ValidationError
8
+
9
+ from acceldata.client.http_client import BaseHttpClient
10
+ from acceldata.client.transient_retry import (
11
+ RetryConfig,
12
+ effective_transient_retry,
13
+ transient_retry_call,
14
+ )
15
+ from acceldata.exceptions import APIError
16
+ from acceldata.models.api.catalog.asset import Asset
17
+ from acceldata.models.api.catalog.asset_label_request import AssetLabelRequest
18
+ from acceldata.models.api.catalog.asset_tag_info import AssetTagInfo
19
+ from acceldata.models.api.catalog.asset_tag_request import AssetTagRequest
20
+ from acceldata.models.api.catalog.asset_tags_response import AssetTagsResponse
21
+ from acceldata.models.api.catalog.catalog_asset_meta_data import CatalogAssetMetaData
22
+ from acceldata.models.api.catalog.label import Label
23
+ from acceldata.models.api.catalog.mini_profile_request import MiniProfileRequest
24
+ from acceldata.models.api.catalog.profile_request import ProfileRequest
25
+ from acceldata.models.api.catalog.sample_data_api_response import SampleDataApiResponse
26
+ from acceldata.models.sdk.catalog.asset_resource import AssetResource
27
+ from acceldata.models.sdk.catalog.profiling import AssetProfilingParams
28
+ from acceldata.models.sdk.catalog.profile_request_resource import ProfileRequestResource
29
+
30
+ logger = logging.getLogger("assetservice")
31
+
32
+ # When sync=True, poll until the profile is clearly finished. The catalog can use
33
+ # many non-terminal `status` values (PENDING, SCHEDULED, …) and may mirror progress
34
+ # on `analysisJob.executionStatus`; a fixed in-progress set misses those and returns too early.
35
+ _PROFILE_REQUEST_TERMINAL_STATUSES = frozenset(
36
+ {
37
+ "ABORTED",
38
+ "CANCELED",
39
+ "CANCELLED",
40
+ "COMPLETE",
41
+ "COMPLETED",
42
+ "ERRORED",
43
+ "EXECUTION_RESULT",
44
+ "FAILED",
45
+ "FINISHED",
46
+ "SKIPPED",
47
+ "SUCCESS",
48
+ "SUCCEEDED",
49
+ "SUCCESSFUL",
50
+ "WARNING",
51
+ }
52
+ )
53
+ _DATAPLANE_EXECUTION_TERMINAL_STATUSES = frozenset(
54
+ {
55
+ "ABORTED",
56
+ "EXECUTION_RESULT",
57
+ "FAILED",
58
+ "SKIPPED",
59
+ "SUCCESSFUL",
60
+ }
61
+ )
62
+
63
+
64
+ def _norm_catalog_enum(s: str) -> str:
65
+ """Uppercase and collapse spaces to underscores (``IN PROGRESS`` → ``IN_PROGRESS``)."""
66
+ return (s or "").upper().replace(" ", "_")
67
+
68
+
69
+ def _profile_request_reached_terminal_state(pr: ProfileRequest) -> bool:
70
+ st = _norm_catalog_enum(pr.status or "")
71
+ if st in _PROFILE_REQUEST_TERMINAL_STATUSES:
72
+ return True
73
+ job = pr.analysis_job
74
+ if job is not None and job.execution_status:
75
+ ex = _norm_catalog_enum(job.execution_status or "")
76
+ if ex in _DATAPLANE_EXECUTION_TERMINAL_STATUSES:
77
+ return True
78
+ return False
79
+
80
+ def _asset_list_from_response(response: dict[str, Any]) -> list[Any]:
81
+ for key in ("data", "assets"):
82
+ raw = response.get(key)
83
+ if raw is None:
84
+ continue
85
+ if isinstance(raw, (list, tuple)):
86
+ return list(raw)
87
+ raise KeyError(
88
+ "assets response must include 'data' or 'assets' with a list"
89
+ )
90
+
91
+
92
+ class AssetService:
93
+ def __init__(self, http_client: BaseHttpClient) -> None:
94
+ self._client = http_client
95
+
96
+ def _catalog_body_error(
97
+ self,
98
+ *,
99
+ operation: str,
100
+ url: str,
101
+ response: object,
102
+ reason: str,
103
+ ) -> None:
104
+ """Raise :class:`APIError` when the catalog returns 2xx but the body is unusable."""
105
+ raise APIError(
106
+ message=f"{operation}: {reason}",
107
+ status_code=200,
108
+ response=response,
109
+ url=url,
110
+ )
111
+
112
+ @staticmethod
113
+ def _identifier_is_int_asset_id(identifier: str | int) -> bool:
114
+ """True when ``identifier`` should be resolved as a numeric asset id (not a uid string)."""
115
+ if isinstance(identifier, bool):
116
+ return False
117
+ if isinstance(identifier, int):
118
+ return True
119
+ if isinstance(identifier, str):
120
+ s = identifier.strip()
121
+ if s == "":
122
+ return False
123
+ try:
124
+ int(s)
125
+ return True
126
+ except ValueError:
127
+ return False
128
+ return False
129
+
130
+ def _asset_dicts_from_list_response(self, response: object) -> list[dict[str, Any]]:
131
+ """Read asset rows from ``GET /assets?...`` JSON (``data`` list or ``data.assets``)."""
132
+ if not isinstance(response, dict):
133
+ return []
134
+ data = response.get("data")
135
+ logger.debug(f"data: {data}")
136
+ if isinstance(data, list):
137
+ return [x for x in data if isinstance(x, dict)]
138
+ if isinstance(data, dict):
139
+ nested = data.get("assets")
140
+ if isinstance(nested, list):
141
+ return [x for x in nested if isinstance(x, dict)]
142
+ return []
143
+
144
+ def _wrap_asset(self, asset: Asset) -> AssetResource:
145
+ return AssetResource(asset_service=self, model=asset)
146
+
147
+ def _first_asset_dict(self, response: object) -> dict[str, Any] | None:
148
+ rows = self._asset_dicts_from_list_response(response)
149
+ return rows[0] if rows else None
150
+
151
+ def get_asset(self, identifier: str | int) -> AssetResource:
152
+ """
153
+ Find an asset by uid or numeric id.
154
+
155
+ :param identifier: Asset uid, or numeric id (``int`` or decimal string).
156
+ """
157
+ if identifier is None or (isinstance(identifier, str) and identifier.strip() == ""):
158
+ raise ValueError("asset identifier is required")
159
+ logger.debug("get_asset identifier=%r", identifier)
160
+ if self._identifier_is_int_asset_id(identifier):
161
+ aid = int(identifier) if isinstance(identifier, str) else int(identifier)
162
+ return self.get_asset_by_id(aid)
163
+ return self.get_asset_by_uid(str(identifier).strip())
164
+
165
+ def get_asset_by_id(self, id: int) -> AssetResource:
166
+ if id is None:
167
+ raise ValueError("Asset id is required")
168
+ logger.debug("get_asset_by_id id=%s", id)
169
+ url = self._client.catalog_urls.assets_by_ids(asset_ids=id)
170
+ response = self._client._get(url)
171
+ logger.debug(f"get_asset_by_id response: {response}")
172
+ if not isinstance(response, dict):
173
+ self._catalog_body_error(
174
+ operation="get_asset_by_id",
175
+ url=url,
176
+ response=response,
177
+ reason="expected JSON object response",
178
+ )
179
+ row = self._first_asset_dict(response)
180
+ asset = Asset.from_dict(row)
181
+ if asset is None:
182
+ self._catalog_body_error(
183
+ operation="get_asset_by_id",
184
+ url=url,
185
+ response=response,
186
+ reason="no asset in response body",
187
+ )
188
+ logger.debug("get_asset_by_id: parsed asset id=%s asset_id=%s", id, asset.id)
189
+ return self._wrap_asset(asset)
190
+
191
+ def get_asset_by_uid(self, uid: str) -> AssetResource:
192
+ url = self._client.catalog_urls.assets_by_uid(uid=uid)
193
+ response = self._client._get(url)
194
+ logger.debug(f"get_asset_by_uid :{response}")
195
+ if not isinstance(response, dict):
196
+ self._catalog_body_error(
197
+ operation="get_asset_by_uid",
198
+ url=url,
199
+ response=response,
200
+ reason="expected JSON object response",
201
+ )
202
+
203
+ row = self._first_asset_dict(response)
204
+ logger.debug(f"asset row: {row}")
205
+ asset = Asset.from_dict(row)
206
+ if asset is None:
207
+ self._catalog_body_error(
208
+ operation="get_asset_by_uid",
209
+ url=url,
210
+ response=response,
211
+ reason="no asset in response body",
212
+ )
213
+ return self._wrap_asset(asset)
214
+
215
+
216
+ def get_asset_metadata(self, asset_id: int) -> CatalogAssetMetaData:
217
+ logger.debug("get_asset_metadata asset_id=%s", asset_id)
218
+ url = self._client.catalog_urls.asset_metadata(asset_id=asset_id)
219
+ response = self._client._get(url=url)
220
+ if not isinstance(response, dict):
221
+ self._catalog_body_error(
222
+ operation="get_asset_metadata",
223
+ url=url,
224
+ response=response,
225
+ reason="expected JSON object response",
226
+ )
227
+ data = response.get("data")
228
+ logger.debug("asset meta data: %s", data)
229
+ if data is None:
230
+ self._catalog_body_error(
231
+ operation="get_asset_metadata",
232
+ url=url,
233
+ response=response,
234
+ reason="missing 'data' in response body",
235
+ )
236
+ meta = CatalogAssetMetaData.from_dict(data)
237
+ if meta is None:
238
+ self._catalog_body_error(
239
+ operation="get_asset_metadata",
240
+ url=url,
241
+ response=response,
242
+ reason="could not parse catalog asset metadata",
243
+ )
244
+ return meta
245
+
246
+
247
+ def add_custom_metadata(
248
+ self,
249
+ asset_id: int,
250
+ custom_metadata: Mapping[str, str],
251
+ ) -> CatalogAssetMetaData:
252
+ """POST custom metadata as string key/value pairs (JSON object with string values)."""
253
+ logger.debug(
254
+ "add_custom_metadata asset_id=%s keys=%s",
255
+ asset_id,
256
+ list(custom_metadata.keys()),
257
+ )
258
+ url = self._client.catalog_urls.asset_custom_metadata(asset_id=asset_id)
259
+ response = self._client._post(url, payload=dict(custom_metadata))
260
+ logger.debug(response)
261
+ if not isinstance(response, dict):
262
+ self._catalog_body_error(
263
+ operation="add_custom_metadata",
264
+ url=url,
265
+ response=response,
266
+ reason="expected JSON object response",
267
+ )
268
+ data = response.get("data")
269
+ logger.debug("asset meta data: %s", data)
270
+ if data is None:
271
+ self._catalog_body_error(
272
+ operation="add_custom_metadata",
273
+ url=url,
274
+ response=response,
275
+ reason="missing 'data' in response body",
276
+ )
277
+ meta = CatalogAssetMetaData.from_dict(data)
278
+ if meta is None:
279
+ self._catalog_body_error(
280
+ operation="add_custom_metadata",
281
+ url=url,
282
+ response=response,
283
+ reason="could not parse catalog asset metadata",
284
+ )
285
+ return meta
286
+
287
+ def _profile_request_from_catalog_response(
288
+ self,
289
+ response: object,
290
+ *,
291
+ op: str,
292
+ asset_id: int,
293
+ url: str,
294
+ ) -> ProfileRequest:
295
+ if not isinstance(response, dict):
296
+ self._catalog_body_error(
297
+ operation=op,
298
+ url=url,
299
+ response=response,
300
+ reason=f"non-dict response (asset_id={asset_id})",
301
+ )
302
+ # POST /assets/{id}/profile returns StandardResponse_ProfileRequest (``data``).
303
+ # GET /assets/{id}/profile/{reqId} returns ProfileRequestResponse (``profileRequest``).
304
+ data = response.get("data")
305
+ if data is None:
306
+ data = response.get("profileRequest")
307
+ result = ProfileRequest.from_dict(data)
308
+ if result is None:
309
+ self._catalog_body_error(
310
+ operation=op,
311
+ url=url,
312
+ response=response,
313
+ reason=f"missing or invalid profile request data (asset_id={asset_id})",
314
+ )
315
+ return result
316
+
317
+ def _mini_profile_request_from_catalog_response(
318
+ self,
319
+ response: object,
320
+ *,
321
+ op: str,
322
+ asset_id: int,
323
+ url: str,
324
+ ) -> MiniProfileRequest:
325
+ """Parse ``StandardResponse_MiniProfileRequest`` (GET latest profile on ``/assets/{id}/profile``)."""
326
+ if not isinstance(response, dict):
327
+ self._catalog_body_error(
328
+ operation=op,
329
+ url=url,
330
+ response=response,
331
+ reason=f"non-dict response (asset_id={asset_id})",
332
+ )
333
+ data = response.get("data")
334
+ result = MiniProfileRequest.from_dict(data)
335
+ if result is None:
336
+ self._catalog_body_error(
337
+ operation=op,
338
+ url=url,
339
+ response=response,
340
+ reason=f"missing or invalid mini profile request data (asset_id={asset_id})",
341
+ )
342
+ return result
343
+
344
+ def profile_asset(
345
+ self,
346
+ asset_id: int,
347
+ profiling_params: Optional[AssetProfilingParams] = None,
348
+ *,
349
+ sync: bool = False,
350
+ transient_retry: RetryConfig | None = None,
351
+ ) -> ProfileRequestResource:
352
+ """POST ``/assets/{id}/profile`` with a catalog profiling body (``profile_asset`` parity).
353
+
354
+ When ``transient_retry`` is set, the POST retries transient HTTP/transport failures
355
+ before surfacing an error. HTTP 409 is included in the default transient
356
+ ``retry_status_codes``.
357
+
358
+ When ``sync`` is ``True``, this method polls
359
+ ``GET /assets/{id}/profile/{reqId}`` until the top-level request ``status`` or, when
360
+ present, the nested ``analysisJob.executionStatus`` reflects a **terminal** state
361
+ (e.g. ``SUCCESSFUL`` / ``FAILED``), not only a fixed in-progress set.
362
+ """
363
+ if profiling_params is None:
364
+ raise TypeError("profiling_params is required for profile_asset")
365
+ logger.debug("Marker config: %s", profiling_params.marker_config)
366
+ logger.debug("Converting profiling_params to dictionary")
367
+ start_profiling_request = profiling_params.to_start_profiling_request()
368
+ payload = start_profiling_request.to_dict() or {}
369
+ logger.debug("StartProfilingRequest to dictionary: %s", payload)
370
+ logger.debug("Executing Profiling with the following payload: %s", payload)
371
+ url = self._client.catalog_urls.asset_profile(asset_id=asset_id)
372
+ tr_cfg = effective_transient_retry(transient_retry, None)
373
+ try:
374
+ response = transient_retry_call(
375
+ lambda: self._client._post(url, payload=payload or {}),
376
+ tr_cfg,
377
+ logger=logger,
378
+ operation_name="profile_asset",
379
+ )
380
+ logger.debug(f"profile_asset response: {response}")
381
+ model = self._profile_request_from_catalog_response(
382
+ response, op="profile_asset", asset_id=asset_id, url=url
383
+ )
384
+ if sync:
385
+ model = self._poll_profile_request_until_terminal(
386
+ asset_id=asset_id, req_id=int(model.id)
387
+ )
388
+ status = model.status or "UNKNOWN"
389
+ if sync:
390
+ message = (
391
+ f"Profiling completed for asset {asset_id} with request {model.id} "
392
+ f"(status={status})."
393
+ )
394
+ else:
395
+ message = (
396
+ f"Profiling triggered for asset {asset_id} with request {model.id} "
397
+ f"(current status={status})."
398
+ )
399
+ return ProfileRequestResource(self, model, operation_message=message)
400
+ except APIError as exc:
401
+ logger.error(
402
+ "profile_asset: request failed for asset_id=%s (HTTP %s): %s",
403
+ asset_id,
404
+ exc.status_code,
405
+ exc.message,
406
+ )
407
+ raise
408
+
409
+ def _poll_profile_request_until_terminal(self, *, asset_id: int, req_id: int) -> ProfileRequest:
410
+ """Poll profile request details until top-level or dataplane status is terminal."""
411
+ profile_request = self.get_profile_request_details(asset_id=asset_id, req_id=req_id)
412
+ while not _profile_request_reached_terminal_state(profile_request):
413
+ time.sleep(5)
414
+ profile_request = self.get_profile_request_details(asset_id=asset_id, req_id=req_id)
415
+ return profile_request
416
+
417
+ def cancel_profile(self, profile_req_id: int):
418
+ return self._client.cancel_profile(profile_req_id)
419
+
420
+ def get_asset_tags(self, asset_id: int) -> list[AssetTagInfo]:
421
+ """GET tags for one asset (``.../assets/{id}/tags``)."""
422
+ if asset_id is None:
423
+ raise ValueError("Asset id is required")
424
+ logger.debug("get_asset_tags asset_id=%s", asset_id)
425
+ url = self._client.catalog_urls.asset_tags(asset_id=asset_id)
426
+ response = self._client._get(url=url)
427
+ return self._asset_tags_from_catalog_response(
428
+ response, operation="get_asset_tags", url=url
429
+ )
430
+
431
+ def add_asset_tag(self, asset_id: int, payload: AssetTagRequest) -> AssetTagInfo:
432
+ """POST a tag on an asset (``.../assets/{id}/tag``)."""
433
+ if asset_id is None:
434
+ raise ValueError("Asset id is required")
435
+ logger.debug("add_asset_tag asset_id=%s", asset_id)
436
+ url = self._client.catalog_urls.asset_tag(asset_id=asset_id)
437
+ response = self._client._post(url, payload=payload.to_dict())
438
+ if not isinstance(response, dict):
439
+ self._catalog_body_error(
440
+ operation="add_asset_tag",
441
+ url=url,
442
+ response=response,
443
+ reason="expected JSON object response",
444
+ )
445
+ tag = AssetTagInfo.from_dict(response)
446
+ if tag is None:
447
+ self._catalog_body_error(
448
+ operation="add_asset_tag",
449
+ url=url,
450
+ response=response,
451
+ reason="could not parse asset tag from response body",
452
+ )
453
+ return tag
454
+
455
+ def add_asset_labels(self, asset_id: int, payload: AssetLabelRequest) -> list[Label]:
456
+ """PUT labels on an asset (``.../assets/{id}/labels``)."""
457
+ if asset_id is None:
458
+ raise ValueError("Asset id is required")
459
+ logger.debug("add_asset_labels asset_id=%s", asset_id)
460
+ url = self._client.catalog_urls.asset_labels(asset_id=asset_id)
461
+ response = self._client._put(url, payload=payload.to_dict())
462
+ return self._labels_from_catalog_response(
463
+ response, operation="add_asset_labels", url=url
464
+ )
465
+
466
+ def get_asset_labels(self, asset_id: int) -> list[Label]:
467
+ """GET labels for one asset (``.../assets/{id}/labels``)."""
468
+ if asset_id is None:
469
+ raise ValueError("Asset id is required")
470
+ logger.debug("get_asset_labels asset_id=%s", asset_id)
471
+ url = self._client.catalog_urls.asset_labels(asset_id=asset_id)
472
+ response = self._client._get(url=url)
473
+ return self._labels_from_catalog_response(
474
+ response, operation="get_asset_labels", url=url
475
+ )
476
+
477
+ @staticmethod
478
+ def _asset_tags_from_catalog_response(
479
+ response: object, *, operation: str, url: str
480
+ ) -> list[AssetTagInfo]:
481
+ if isinstance(response, dict):
482
+ inner = response.get("data")
483
+ if isinstance(inner, dict):
484
+ response = inner
485
+ try:
486
+ parsed = AssetTagsResponse.from_dict(response)
487
+ if parsed is not None and parsed.tags is not None:
488
+ return list(parsed.tags)
489
+ except (ValidationError, ValueError, TypeError):
490
+ pass
491
+ for key in ("tags", "assetTags"):
492
+ raw = response.get(key)
493
+ if isinstance(raw, list):
494
+ tags: list[AssetTagInfo] = []
495
+ for item in raw:
496
+ if not isinstance(item, dict):
497
+ continue
498
+ tag = AssetTagInfo.from_dict(item)
499
+ if tag is not None:
500
+ tags.append(tag)
501
+ return tags
502
+ raise APIError(
503
+ message=f"{operation}: missing or invalid tags in response body",
504
+ status_code=200,
505
+ response=response,
506
+ url=url,
507
+ )
508
+
509
+ @staticmethod
510
+ def _labels_from_catalog_response(
511
+ response: object, *, operation: str, url: str
512
+ ) -> list[Label]:
513
+ raw: list[Any] | None = None
514
+ if isinstance(response, list):
515
+ raw = list(response)
516
+ elif isinstance(response, dict):
517
+ data = response.get("data")
518
+ if isinstance(data, list):
519
+ raw = list(data)
520
+ elif isinstance(data, dict) and isinstance(data.get("labels"), list):
521
+ raw = list(data["labels"])
522
+ elif isinstance(response.get("labels"), list):
523
+ raw = list(response["labels"])
524
+ if raw is None:
525
+ raise APIError(
526
+ message=f"{operation}: expected a JSON array of labels",
527
+ status_code=200,
528
+ response=response,
529
+ url=url,
530
+ )
531
+ labels: list[Label] = []
532
+ for item in raw:
533
+ if not isinstance(item, dict):
534
+ continue
535
+ label = Label.from_dict(item)
536
+ if label is not None:
537
+ labels.append(label)
538
+ return labels
539
+
540
+ def get_tags(self) -> AssetTagsResponse | None:
541
+ logger.debug("get_tags (global)")
542
+ url = self._client.catalog_urls.assets_tags()
543
+ response = self._client._get(url=url)
544
+ if not isinstance(response, dict):
545
+ return None
546
+ inner = response.get("data")
547
+ if isinstance(inner, dict):
548
+ try:
549
+ return AssetTagsResponse.from_dict(inner)
550
+ except (ValidationError, ValueError, TypeError):
551
+ pass
552
+ try:
553
+ return AssetTagsResponse.from_dict(response)
554
+ except (ValidationError, ValueError, TypeError):
555
+ logger.debug("get_tags: AssetTagsResponse.from_dict failed", exc_info=True)
556
+ return None
557
+
558
+ def get_latest_profile_status(self, asset_id: int) -> MiniProfileRequest:
559
+ """GET latest profile status for the asset (same path as profile POST; body is ``MiniProfileRequest``)."""
560
+ logger.debug("get_latest_profile_status asset_id=%s", asset_id)
561
+ url = self._client.catalog_urls.asset_profile(asset_id=asset_id)
562
+ response = self._client._get(url=url)
563
+ return self._mini_profile_request_from_catalog_response(
564
+ response, op="get_latest_profile_status", asset_id=asset_id, url=url
565
+ )
566
+
567
+ def get_profile_request_details(self, asset_id: int, req_id: int) -> ProfileRequest:
568
+ """GET one profile request by asset id and profile request id."""
569
+ logger.debug(
570
+ "get_profile_request_details asset_id=%s req_id=%s", asset_id, req_id
571
+ )
572
+ url = self._client.catalog_urls.profile_request_details(
573
+ asset_id=asset_id, req_id=req_id
574
+ )
575
+ response = self._client._get(url=url)
576
+ return self._profile_request_from_catalog_response(
577
+ response,
578
+ op="get_profile_request_details",
579
+ asset_id=asset_id,
580
+ url=url,
581
+ )
582
+
583
+
584
+ def get_sample_data_result(
585
+ self,
586
+ request_id: str | SampleDataApiResponse | None,
587
+ ) -> SampleDataApiResponse | None:
588
+ if isinstance(request_id, SampleDataApiResponse):
589
+ initial = request_id
590
+ rid = initial.request_id
591
+ if not rid:
592
+ logger.debug(
593
+ "get_sample_data_result: inline sample response job_execution_status=%s",
594
+ initial.job_execution_status,
595
+ )
596
+ return initial
597
+ request_id = rid
598
+
599
+ if not request_id:
600
+ raise ValueError("request_id is null")
601
+
602
+ logger.debug("get_sample_data_result request_id=%s", request_id)
603
+ url = self._client.catalog_urls.asset_sample_result(request_id=request_id)
604
+ response = self._client._get(url=url)
605
+ if not isinstance(response, dict):
606
+ logger.error("get_sample_data_result: non-dict response request_id=%s", request_id)
607
+ return SampleDataApiResponse.from_dict(response)
608
+
609
+ def sample_data(
610
+ self,
611
+ asset_id: int,
612
+ *,
613
+ sync: bool = True,
614
+ ) -> SampleDataApiResponse | None:
615
+ """Trigger sample data and optionally poll for completion.
616
+
617
+ When ``sync`` is ``True`` (default), this method blocks and polls
618
+ ``get_sample_data_result`` until ``jobExecutionStatus`` is not
619
+ ``IN_PROGRESS``. When ``sync`` is ``False``, it returns the initial
620
+ async response (typically containing ``request_id``) without polling.
621
+ """
622
+ logger.debug("sample_data asset_id=%s sync=%s", asset_id, sync)
623
+ url = self._client.catalog_urls.asset_sample_async(asset_id=asset_id)
624
+ response = self._client._post(url, payload={})
625
+ if not isinstance(response, dict):
626
+ logger.debug("sample_data: non-dict async response asset_id=%s", asset_id)
627
+ return None
628
+ try:
629
+ initial = SampleDataApiResponse.from_dict(response)
630
+ except (ValidationError, ValueError, TypeError):
631
+ logger.debug("sample_data: parse async response failed asset_id=%s", asset_id, exc_info=True)
632
+ return None
633
+ if initial is None:
634
+ return None
635
+ rid = initial.request_id
636
+ if not rid:
637
+ logger.debug("sample_data: no request_id in async response asset_id=%s", asset_id)
638
+ return initial
639
+ if not sync:
640
+ return initial
641
+
642
+ sample_data_result = self.get_sample_data_result(rid)
643
+ while (
644
+ sample_data_result is not None
645
+ and (sample_data_result.job_execution_status or "") == "IN_PROGRESS"
646
+ ):
647
+ time.sleep(5)
648
+ sample_data_result = self.get_sample_data_result(rid)
649
+ return sample_data_result
650
+
651
+