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.
- acceldata/__init__.py +7 -0
- acceldata/client/__init__.py +0 -0
- acceldata/client/adoc_client.py +666 -0
- acceldata/client/http_client.py +346 -0
- acceldata/client/transient_retry.py +142 -0
- acceldata/constants/__init__.py +1 -0
- acceldata/constants/constants.py +13 -0
- acceldata/exceptions/__init__.py +52 -0
- acceldata/exceptions/api_errors.py +85 -0
- acceldata/exceptions/exceptions.py +174 -0
- acceldata/exceptions/sdk_exceptions.py +98 -0
- acceldata/mappers/__init__.py +21 -0
- acceldata/mappers/catalog_executions.py +107 -0
- acceldata/models/__init__.py +0 -0
- acceldata/models/api/catalog/above_configs.py +99 -0
- acceldata/models/api/catalog/above_configs_success.py +138 -0
- acceldata/models/api/catalog/absolute_configs.py +197 -0
- acceldata/models/api/catalog/absolute_configs_fallback.py +101 -0
- acceldata/models/api/catalog/absolute_threshold_config.py +105 -0
- acceldata/models/api/catalog/analysis_job_context.py +102 -0
- acceldata/models/api/catalog/analytics_pipeline.py +385 -0
- acceldata/models/api/catalog/analytics_pipeline_endpoint.py +136 -0
- acceldata/models/api/catalog/anomaly_config.py +95 -0
- acceldata/models/api/catalog/anomaly_details.py +119 -0
- acceldata/models/api/catalog/anomaly_threshold_config.py +105 -0
- acceldata/models/api/catalog/artifact.py +92 -0
- acceldata/models/api/catalog/assembly.py +171 -0
- acceldata/models/api/catalog/asset.py +162 -0
- acceldata/models/api/catalog/asset_catalog_listing.py +159 -0
- acceldata/models/api/catalog/asset_catalog_listing_response.py +102 -0
- acceldata/models/api/catalog/asset_label_request.py +96 -0
- acceldata/models/api/catalog/asset_progress.py +96 -0
- acceldata/models/api/catalog/asset_progress_details.py +117 -0
- acceldata/models/api/catalog/asset_tag.py +124 -0
- acceldata/models/api/catalog/asset_tag_info.py +140 -0
- acceldata/models/api/catalog/asset_tag_request.py +95 -0
- acceldata/models/api/catalog/asset_tag_response.py +96 -0
- acceldata/models/api/catalog/asset_tags_response.py +102 -0
- acceldata/models/api/catalog/asset_type.py +96 -0
- acceldata/models/api/catalog/auto_anomaly_feature_analysis.py +160 -0
- acceldata/models/api/catalog/auto_anomaly_item_execution.py +133 -0
- acceldata/models/api/catalog/auto_anomaly_meta_data.py +162 -0
- acceldata/models/api/catalog/auto_anomaly_rule_execution.py +102 -0
- acceldata/models/api/catalog/auto_anomaly_summary.py +124 -0
- acceldata/models/api/catalog/backing_asset.py +126 -0
- acceldata/models/api/catalog/below_configs.py +99 -0
- acceldata/models/api/catalog/big_decimal.py +100 -0
- acceldata/models/api/catalog/big_integer.py +98 -0
- acceldata/models/api/catalog/bounds_date_time_marker_config.py +110 -0
- acceldata/models/api/catalog/bounds_file_event_marker_config.py +108 -0
- acceldata/models/api/catalog/bounds_id_marker_config.py +96 -0
- acceldata/models/api/catalog/catalog_asset_meta_data.py +128 -0
- acceldata/models/api/catalog/changes_by_config.py +105 -0
- acceldata/models/api/catalog/column_mapping.py +139 -0
- acceldata/models/api/catalog/column_metadata.py +90 -0
- acceldata/models/api/catalog/column_metric_with_anomaly_details.py +119 -0
- acceldata/models/api/catalog/column_variable.py +111 -0
- acceldata/models/api/catalog/composite_policy_execution.py +102 -0
- acceldata/models/api/catalog/composite_policy_item_execution.py +104 -0
- acceldata/models/api/catalog/connection.py +118 -0
- acceldata/models/api/catalog/connection_test_result.py +106 -0
- acceldata/models/api/catalog/connection_type.py +90 -0
- acceldata/models/api/catalog/connection_types_response.py +96 -0
- acceldata/models/api/catalog/crawler.py +88 -0
- acceldata/models/api/catalog/crawler_action_response.py +90 -0
- acceldata/models/api/catalog/crawler_execution.py +147 -0
- acceldata/models/api/catalog/crawler_execution_response.py +102 -0
- acceldata/models/api/catalog/crawler_status_response.py +90 -0
- acceldata/models/api/catalog/create_profile.py +194 -0
- acceldata/models/api/catalog/custom_sql_config.py +121 -0
- acceldata/models/api/catalog/data_bricks_spark_resource_config.py +104 -0
- acceldata/models/api/catalog/data_cadence_rule.py +236 -0
- acceldata/models/api/catalog/data_cadence_rule_details.py +125 -0
- acceldata/models/api/catalog/data_cadence_rule_execution.py +102 -0
- acceldata/models/api/catalog/data_cadence_rule_item.py +104 -0
- acceldata/models/api/catalog/data_cadence_rule_item_execution.py +158 -0
- acceldata/models/api/catalog/data_cadence_rule_response.py +98 -0
- acceldata/models/api/catalog/data_drift_execution.py +102 -0
- acceldata/models/api/catalog/data_drift_item_execution.py +121 -0
- acceldata/models/api/catalog/data_quality_engine.py +90 -0
- acceldata/models/api/catalog/data_quality_execution.py +106 -0
- acceldata/models/api/catalog/data_quality_rule.py +323 -0
- acceldata/models/api/catalog/data_quality_rule_details.py +186 -0
- acceldata/models/api/catalog/data_quality_rule_response.py +120 -0
- acceldata/models/api/catalog/data_quality_rules_response.py +102 -0
- acceldata/models/api/catalog/databricks_compute_config.py +113 -0
- acceldata/models/api/catalog/dataplane_engine.py +105 -0
- acceldata/models/api/catalog/dataplane_health_metrics.py +101 -0
- acceldata/models/api/catalog/dataplane_job.py +216 -0
- acceldata/models/api/catalog/dataplane_job_status.py +113 -0
- acceldata/models/api/catalog/dataplane_request.py +96 -0
- acceldata/models/api/catalog/dataproc_compute_config.py +106 -0
- acceldata/models/api/catalog/dataproc_spark_resource_config.py +104 -0
- acceldata/models/api/catalog/date_based_partition_marker_config.py +116 -0
- acceldata/models/api/catalog/date_partitioning_marker_config.py +113 -0
- acceldata/models/api/catalog/date_time_marker_config.py +109 -0
- acceldata/models/api/catalog/day_month_year_based_partition_marker_config.py +124 -0
- acceldata/models/api/catalog/decreased_by_config.py +105 -0
- acceldata/models/api/catalog/delete_artifact_response.py +88 -0
- acceldata/models/api/catalog/dq_rule_execution.py +248 -0
- acceldata/models/api/catalog/drift.py +121 -0
- acceldata/models/api/catalog/enable_rule_request.py +88 -0
- acceldata/models/api/catalog/engine.py +90 -0
- acceldata/models/api/catalog/execution_cancellation_response.py +92 -0
- acceldata/models/api/catalog/execution_response.py +144 -0
- acceldata/models/api/catalog/execution_result.py +126 -0
- acceldata/models/api/catalog/executions_response.py +102 -0
- acceldata/models/api/catalog/failed_asset_detail.py +90 -0
- acceldata/models/api/catalog/failed_summary.py +98 -0
- acceldata/models/api/catalog/file.py +94 -0
- acceldata/models/api/catalog/file_marker_config.py +99 -0
- acceldata/models/api/catalog/generic_config_fallback.py +101 -0
- acceldata/models/api/catalog/generic_execution.py +221 -0
- acceldata/models/api/catalog/generic_rule_execution.py +137 -0
- acceldata/models/api/catalog/global_storage_config_dto.py +172 -0
- acceldata/models/api/catalog/hosted_dp_deployment_response.py +98 -0
- acceldata/models/api/catalog/id_marker_config.py +94 -0
- acceldata/models/api/catalog/increased_by_config.py +105 -0
- acceldata/models/api/catalog/item.py +160 -0
- acceldata/models/api/catalog/item_execution.py +192 -0
- acceldata/models/api/catalog/item_segment.py +131 -0
- acceldata/models/api/catalog/job_schedule.py +118 -0
- acceldata/models/api/catalog/job_spec.py +96 -0
- acceldata/models/api/catalog/kubernetes_metrics.py +91 -0
- acceldata/models/api/catalog/label.py +97 -0
- acceldata/models/api/catalog/livy_compute_config.py +103 -0
- acceldata/models/api/catalog/livy_spark_resource_config.py +106 -0
- acceldata/models/api/catalog/look_back_config.py +90 -0
- acceldata/models/api/catalog/manual_tag_request.py +92 -0
- acceldata/models/api/catalog/mapping.py +97 -0
- acceldata/models/api/catalog/marker.py +88 -0
- acceldata/models/api/catalog/marker_config.py +349 -0
- acceldata/models/api/catalog/marker_config_fallback.py +101 -0
- acceldata/models/api/catalog/meta.py +92 -0
- acceldata/models/api/catalog/meta_data.py +92 -0
- acceldata/models/api/catalog/meta_data_item.py +94 -0
- acceldata/models/api/catalog/mini_profile_request.py +112 -0
- acceldata/models/api/catalog/notification_payload.py +114 -0
- acceldata/models/api/catalog/offset_based_marker_config.py +97 -0
- acceldata/models/api/catalog/operational_config.py +92 -0
- acceldata/models/api/catalog/pattern_configuration.py +90 -0
- acceldata/models/api/catalog/pipe_line_request.py +92 -0
- acceldata/models/api/catalog/pipeline_compute_config.py +110 -0
- acceldata/models/api/catalog/policy_execution_marker_config.py +94 -0
- acceldata/models/api/catalog/policy_execution_request.py +229 -0
- acceldata/models/api/catalog/policy_group.py +114 -0
- acceldata/models/api/catalog/policy_transform_udf.py +113 -0
- acceldata/models/api/catalog/process_metrics.py +91 -0
- acceldata/models/api/catalog/profile_anomaly_details_response.py +109 -0
- acceldata/models/api/catalog/profile_anomaly_item_execution.py +127 -0
- acceldata/models/api/catalog/profile_anomaly_rule_execution.py +102 -0
- acceldata/models/api/catalog/profile_anomaly_rule_item.py +109 -0
- acceldata/models/api/catalog/profile_cancellation_response.py +92 -0
- acceldata/models/api/catalog/profile_request.py +211 -0
- acceldata/models/api/catalog/profile_request_response.py +92 -0
- acceldata/models/api/catalog/property_template.py +178 -0
- acceldata/models/api/catalog/property_templates.py +123 -0
- acceldata/models/api/catalog/pub_sub_lookback_marker_config.py +96 -0
- acceldata/models/api/catalog/pub_sub_timestamp_marker_config.py +108 -0
- acceldata/models/api/catalog/quality_rule.py +381 -0
- acceldata/models/api/catalog/quality_rule_execution_metrics.py +114 -0
- acceldata/models/api/catalog/quality_rule_with_latest_execution.py +115 -0
- acceldata/models/api/catalog/range_configs.py +109 -0
- acceldata/models/api/catalog/reconciliation_item.py +102 -0
- acceldata/models/api/catalog/reconciliation_item_execution.py +157 -0
- acceldata/models/api/catalog/reconciliation_rule.py +339 -0
- acceldata/models/api/catalog/reconciliation_rule_details.py +225 -0
- acceldata/models/api/catalog/reconciliation_rule_execution.py +102 -0
- acceldata/models/api/catalog/reconciliation_rule_response.py +98 -0
- acceldata/models/api/catalog/reconciliation_rules_response.py +102 -0
- acceldata/models/api/catalog/relative_config.py +197 -0
- acceldata/models/api/catalog/relative_config_fallback.py +101 -0
- acceldata/models/api/catalog/relative_threshold_config.py +111 -0
- acceldata/models/api/catalog/root_cause_analysis.py +108 -0
- acceldata/models/api/catalog/rule_execution.py +327 -0
- acceldata/models/api/catalog/rule_execution_result.py +90 -0
- acceldata/models/api/catalog/rule_item_result.py +125 -0
- acceldata/models/api/catalog/rule_meta_data.py +94 -0
- acceldata/models/api/catalog/rule_spark_sql_dynamic_filter_variable_mapping.py +100 -0
- acceldata/models/api/catalog/rule_tag.py +101 -0
- acceldata/models/api/catalog/rule_threshold_configuration.py +236 -0
- acceldata/models/api/catalog/rule_threshold_configuration_fallback.py +101 -0
- acceldata/models/api/catalog/rules_with_latest_execution_response.py +102 -0
- acceldata/models/api/catalog/sample_data_api_response.py +122 -0
- acceldata/models/api/catalog/schema_drift_item_execution.py +131 -0
- acceldata/models/api/catalog/schema_drift_rule_execution.py +102 -0
- acceldata/models/api/catalog/secret_manager_configuration.py +90 -0
- acceldata/models/api/catalog/secret_manager_configuration_with_option.py +98 -0
- acceldata/models/api/catalog/security_config.py +90 -0
- acceldata/models/api/catalog/segment_metric_json.py +142 -0
- acceldata/models/api/catalog/segments_analysis.py +145 -0
- acceldata/models/api/catalog/short_segment.py +97 -0
- acceldata/models/api/catalog/simple_quality_rule_response.py +92 -0
- acceldata/models/api/catalog/snapshot_data.py +117 -0
- acceldata/models/api/catalog/source_model.py +90 -0
- acceldata/models/api/catalog/source_type.py +100 -0
- acceldata/models/api/catalog/source_types_response.py +102 -0
- acceldata/models/api/catalog/spark_job_status.py +113 -0
- acceldata/models/api/catalog/spark_resource_config.py +114 -0
- acceldata/models/api/catalog/standard_response_assembly.py +92 -0
- acceldata/models/api/catalog/standard_response_crawler_action_response.py +92 -0
- acceldata/models/api/catalog/standard_response_crawler_status_response.py +92 -0
- acceldata/models/api/catalog/standard_response_delete_artifact_response.py +92 -0
- acceldata/models/api/catalog/standard_response_list.py +96 -0
- acceldata/models/api/catalog/standard_response_map.py +88 -0
- acceldata/models/api/catalog/standard_response_meta_data.py +92 -0
- acceldata/models/api/catalog/standard_response_mini_profile_request.py +92 -0
- acceldata/models/api/catalog/standard_response_profile_request.py +92 -0
- acceldata/models/api/catalog/start_profiling_request.py +92 -0
- acceldata/models/api/catalog/tag_domain.py +116 -0
- acceldata/models/api/catalog/threshold_breach_details.py +101 -0
- acceldata/models/api/catalog/time_based_config.py +92 -0
- acceldata/models/api/catalog/time_based_threshold_config.py +105 -0
- acceldata/models/api/catalog/timestamp_based_marker_config.py +115 -0
- acceldata/models/api/catalog/udf_variable.py +97 -0
- acceldata/models/api/catalog/yunikorn_compute_config.py +133 -0
- acceldata/models/api/catalog/yunikorn_spark_resource_config.py +154 -0
- acceldata/models/api/pipeline/big_decimal.py +100 -0
- acceldata/models/api/pipeline/big_integer.py +98 -0
- acceldata/models/api/pipeline/child_span_response.py +96 -0
- acceldata/models/api/pipeline/create_span_event.py +109 -0
- acceldata/models/api/pipeline/edge.py +108 -0
- acceldata/models/api/pipeline/event_alert_object.py +95 -0
- acceldata/models/api/pipeline/generic_config_fallback.py +101 -0
- acceldata/models/api/pipeline/identifier.py +146 -0
- acceldata/models/api/pipeline/job_request.py +130 -0
- acceldata/models/api/pipeline/meta.py +107 -0
- acceldata/models/api/pipeline/meta_data.py +92 -0
- acceldata/models/api/pipeline/metric.py +103 -0
- acceldata/models/api/pipeline/node.py +201 -0
- acceldata/models/api/pipeline/node_response.py +92 -0
- acceldata/models/api/pipeline/notification_payload.py +114 -0
- acceldata/models/api/pipeline/pipeline.py +152 -0
- acceldata/models/api/pipeline/pipeline_baseline_metric.py +92 -0
- acceldata/models/api/pipeline/pipeline_details.py +118 -0
- acceldata/models/api/pipeline/pipeline_details_response.py +102 -0
- acceldata/models/api/pipeline/pipeline_input_request.py +92 -0
- acceldata/models/api/pipeline/pipeline_listing_info.py +118 -0
- acceldata/models/api/pipeline/pipeline_response.py +92 -0
- acceldata/models/api/pipeline/pipeline_run.py +136 -0
- acceldata/models/api/pipeline/pipeline_run_request.py +92 -0
- acceldata/models/api/pipeline/pipeline_runs_response.py +102 -0
- acceldata/models/api/pipeline/pipeline_summary.py +98 -0
- acceldata/models/api/pipeline/pipelines_listing_response.py +102 -0
- acceldata/models/api/pipeline/policy_summary.py +102 -0
- acceldata/models/api/pipeline/span.py +115 -0
- acceldata/models/api/pipeline/span_event.py +114 -0
- acceldata/models/api/pipeline/span_event_request.py +92 -0
- acceldata/models/api/pipeline/span_event_response.py +92 -0
- acceldata/models/api/pipeline/span_request.py +94 -0
- acceldata/models/api/pipeline/spans_response.py +96 -0
- acceldata/models/api/pipeline/tag.py +97 -0
- acceldata/models/sdk/__init__.py +0 -0
- acceldata/models/sdk/catalog/__init__.py +105 -0
- acceldata/models/sdk/catalog/asset_resource.py +112 -0
- acceldata/models/sdk/catalog/asset_source_type.py +59 -0
- acceldata/models/sdk/catalog/custom_asset_metadata.py +11 -0
- acceldata/models/sdk/catalog/datasource_resource.py +88 -0
- acceldata/models/sdk/catalog/executor.py +524 -0
- acceldata/models/sdk/catalog/policy_execution_request.py +114 -0
- acceldata/models/sdk/catalog/policy_execution_type.py +16 -0
- acceldata/models/sdk/catalog/policy_filter.py +94 -0
- acceldata/models/sdk/catalog/policy_rule_resources.py +388 -0
- acceldata/models/sdk/catalog/profile_request_resource.py +77 -0
- acceldata/models/sdk/catalog/profiling.py +82 -0
- acceldata/models/sdk/catalog/profiling_configuration_error.py +5 -0
- acceldata/models/sdk/pipeline/__init__.py +62 -0
- acceldata/models/sdk/pipeline/create_job_input.py +108 -0
- acceldata/models/sdk/pipeline/create_pipeline_input_request.py +72 -0
- acceldata/models/sdk/pipeline/events/__init__.py +5 -0
- acceldata/models/sdk/pipeline/events/generic_event.py +31 -0
- acceldata/models/sdk/pipeline/events/log_event.py +34 -0
- acceldata/models/sdk/pipeline/events/span_event.py +8 -0
- acceldata/models/sdk/pipeline/pipeline_resource.py +171 -0
- acceldata/models/sdk/pipeline/pipeline_run_input.py +84 -0
- acceldata/models/sdk/pipeline/pipeline_run_resource.py +208 -0
- acceldata/models/sdk/pipeline/pipeline_source_type.py +20 -0
- acceldata/models/sdk/pipeline/span_resource.py +327 -0
- acceldata/openapi_deserialization_patches.py +161 -0
- acceldata/openapi_repr_patches.py +62 -0
- acceldata/services/__init__.py +0 -0
- acceldata/services/api_timestamp.py +31 -0
- acceldata/services/asset_service.py +651 -0
- acceldata/services/datasource_service.py +120 -0
- acceldata/services/generic_service.py +132 -0
- acceldata/services/marker_config.py +232 -0
- acceldata/services/pipeline_big_decimal_api.py +102 -0
- acceldata/services/pipeline_response_parse.py +209 -0
- acceldata/services/pipeline_service.py +494 -0
- acceldata/services/rule_service.py +620 -0
- acceldata/urls/__init__.py +1 -0
- acceldata/urls/catalog_urls.py +137 -0
- acceldata/urls/pipeline_urls.py +55 -0
- acceldata/utils/__init__.py +6 -0
- acceldata/utils/datetime_utils.py +16 -0
- acceldata/utils/operation_ack.py +22 -0
- acceldata/utils/span_helpers.py +95 -0
- acceldata/utils/time_range_utils.py +90 -0
- acceldata_sdk_python-26.7.0.dist-info/METADATA +26 -0
- acceldata_sdk_python-26.7.0.dist-info/RECORD +303 -0
- acceldata_sdk_python-26.7.0.dist-info/WHEEL +5 -0
- acceldata_sdk_python-26.7.0.dist-info/licenses/LICENSE +22 -0
- 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
|
+
|