acryl-datahub 1.2.0.10rc3__py3-none-any.whl → 1.2.0.10rc5__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.
Potentially problematic release.
This version of acryl-datahub might be problematic. Click here for more details.
- {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/METADATA +2513 -2571
- {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/RECORD +94 -87
- {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/entry_points.txt +2 -0
- datahub/_version.py +1 -1
- datahub/api/entities/assertion/assertion.py +1 -1
- datahub/api/entities/corpgroup/corpgroup.py +1 -1
- datahub/api/entities/dataproduct/dataproduct.py +6 -3
- datahub/api/entities/dataset/dataset.py +9 -18
- datahub/api/entities/structuredproperties/structuredproperties.py +2 -2
- datahub/api/graphql/operation.py +10 -6
- datahub/cli/docker_check.py +2 -2
- datahub/configuration/common.py +29 -1
- datahub/configuration/connection_resolver.py +5 -2
- datahub/configuration/import_resolver.py +7 -4
- datahub/configuration/pydantic_migration_helpers.py +0 -9
- datahub/configuration/source_common.py +3 -2
- datahub/configuration/validate_field_deprecation.py +5 -2
- datahub/configuration/validate_field_removal.py +5 -2
- datahub/configuration/validate_field_rename.py +6 -5
- datahub/configuration/validate_multiline_string.py +5 -2
- datahub/ingestion/autogenerated/capability_summary.json +33 -1
- datahub/ingestion/run/pipeline_config.py +2 -2
- datahub/ingestion/source/azure/azure_common.py +1 -1
- datahub/ingestion/source/bigquery_v2/bigquery_config.py +28 -14
- datahub/ingestion/source/bigquery_v2/queries_extractor.py +4 -5
- datahub/ingestion/source/common/gcp_credentials_config.py +3 -1
- datahub/ingestion/source/data_lake_common/path_spec.py +16 -16
- datahub/ingestion/source/datahub/config.py +8 -9
- datahub/ingestion/source/delta_lake/config.py +1 -1
- datahub/ingestion/source/dremio/dremio_config.py +3 -4
- datahub/ingestion/source/feast.py +8 -10
- datahub/ingestion/source/fivetran/config.py +1 -1
- datahub/ingestion/source/ge_profiling_config.py +26 -22
- datahub/ingestion/source/grafana/grafana_config.py +2 -2
- datahub/ingestion/source/grafana/models.py +12 -14
- datahub/ingestion/source/hex/hex.py +6 -1
- datahub/ingestion/source/iceberg/iceberg_profiler.py +4 -2
- datahub/ingestion/source/kafka_connect/common.py +2 -2
- datahub/ingestion/source/looker/looker_common.py +1 -1
- datahub/ingestion/source/looker/looker_config.py +15 -4
- datahub/ingestion/source/looker/looker_source.py +52 -3
- datahub/ingestion/source/looker/lookml_config.py +1 -1
- datahub/ingestion/source/metadata/business_glossary.py +7 -7
- datahub/ingestion/source/metadata/lineage.py +1 -1
- datahub/ingestion/source/mode.py +13 -5
- datahub/ingestion/source/nifi.py +1 -1
- datahub/ingestion/source/powerbi/config.py +14 -21
- datahub/ingestion/source/preset.py +1 -1
- datahub/ingestion/source/qlik_sense/data_classes.py +28 -8
- datahub/ingestion/source/redshift/config.py +6 -3
- datahub/ingestion/source/salesforce.py +13 -9
- datahub/ingestion/source/schema/json_schema.py +14 -14
- datahub/ingestion/source/sigma/data_classes.py +3 -0
- datahub/ingestion/source/snaplogic/__init__.py +0 -0
- datahub/ingestion/source/snaplogic/snaplogic.py +355 -0
- datahub/ingestion/source/snaplogic/snaplogic_config.py +37 -0
- datahub/ingestion/source/snaplogic/snaplogic_lineage_extractor.py +107 -0
- datahub/ingestion/source/snaplogic/snaplogic_parser.py +168 -0
- datahub/ingestion/source/snaplogic/snaplogic_utils.py +31 -0
- datahub/ingestion/source/snowflake/snowflake_config.py +12 -15
- datahub/ingestion/source/snowflake/snowflake_connection.py +8 -3
- datahub/ingestion/source/snowflake/snowflake_lineage_v2.py +15 -2
- datahub/ingestion/source/snowflake/snowflake_queries.py +4 -5
- datahub/ingestion/source/sql/athena.py +2 -1
- datahub/ingestion/source/sql/clickhouse.py +12 -7
- datahub/ingestion/source/sql/cockroachdb.py +5 -3
- datahub/ingestion/source/sql/druid.py +2 -2
- datahub/ingestion/source/sql/hive.py +4 -3
- datahub/ingestion/source/sql/hive_metastore.py +7 -9
- datahub/ingestion/source/sql/mssql/source.py +2 -2
- datahub/ingestion/source/sql/mysql.py +2 -2
- datahub/ingestion/source/sql/oracle.py +3 -3
- datahub/ingestion/source/sql/presto.py +2 -1
- datahub/ingestion/source/sql/teradata.py +4 -4
- datahub/ingestion/source/sql/trino.py +2 -1
- datahub/ingestion/source/sql/two_tier_sql_source.py +2 -3
- datahub/ingestion/source/sql/vertica.py +1 -1
- datahub/ingestion/source/sql_queries.py +6 -6
- datahub/ingestion/source/state/checkpoint.py +5 -1
- datahub/ingestion/source/state/entity_removal_state.py +5 -2
- datahub/ingestion/source/state/stateful_ingestion_base.py +5 -8
- datahub/ingestion/source/superset.py +1 -2
- datahub/ingestion/source/tableau/tableau.py +20 -6
- datahub/ingestion/source/unity/config.py +7 -3
- datahub/ingestion/source/usage/usage_common.py +3 -3
- datahub/ingestion/source_config/pulsar.py +3 -1
- datahub/ingestion/transformer/set_browse_path.py +112 -0
- datahub/sdk/_shared.py +126 -0
- datahub/sdk/chart.py +87 -30
- datahub/sdk/dashboard.py +79 -32
- datahub/sdk/search_filters.py +1 -7
- {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/WHEEL +0 -0
- {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/licenses/LICENSE +0 -0
- {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/top_level.txt +0 -0
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import re
|
|
4
4
|
import time
|
|
5
5
|
from collections import OrderedDict, defaultdict
|
|
6
|
+
from copy import deepcopy
|
|
6
7
|
from dataclasses import dataclass, field as dataclass_field
|
|
7
8
|
from datetime import datetime, timedelta, timezone
|
|
8
9
|
from functools import lru_cache
|
|
@@ -474,6 +475,13 @@ class TableauPageSizeConfig(ConfigModel):
|
|
|
474
475
|
return self.database_table_page_size or self.page_size
|
|
475
476
|
|
|
476
477
|
|
|
478
|
+
_IngestHiddenAssetsOptionsType = Literal["worksheet", "dashboard"]
|
|
479
|
+
_IngestHiddenAssetsOptions: List[_IngestHiddenAssetsOptionsType] = [
|
|
480
|
+
"worksheet",
|
|
481
|
+
"dashboard",
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
|
|
477
485
|
class TableauConfig(
|
|
478
486
|
DatasetLineageProviderConfigBase,
|
|
479
487
|
StatefulIngestionConfigBase,
|
|
@@ -586,13 +594,13 @@ class TableauConfig(
|
|
|
586
594
|
)
|
|
587
595
|
|
|
588
596
|
extract_lineage_from_unsupported_custom_sql_queries: bool = Field(
|
|
589
|
-
default=
|
|
590
|
-
description="[Experimental]
|
|
597
|
+
default=True,
|
|
598
|
+
description="[Experimental] Extract lineage from Custom SQL queries using DataHub's SQL parser in cases where the Tableau Catalog API fails to return lineage for the query.",
|
|
591
599
|
)
|
|
592
600
|
|
|
593
601
|
force_extraction_of_lineage_from_custom_sql_queries: bool = Field(
|
|
594
602
|
default=False,
|
|
595
|
-
description="[Experimental] Force extraction of lineage from
|
|
603
|
+
description="[Experimental] Force extraction of lineage from Custom SQL queries using DataHub's SQL parser, even when the Tableau Catalog API returns lineage already.",
|
|
596
604
|
)
|
|
597
605
|
|
|
598
606
|
sql_parsing_disable_schema_awareness: bool = Field(
|
|
@@ -625,8 +633,8 @@ class TableauConfig(
|
|
|
625
633
|
description="Configuration settings for ingesting Tableau groups and their capabilities as custom properties.",
|
|
626
634
|
)
|
|
627
635
|
|
|
628
|
-
ingest_hidden_assets: Union[List[
|
|
629
|
-
|
|
636
|
+
ingest_hidden_assets: Union[List[_IngestHiddenAssetsOptionsType], bool] = Field(
|
|
637
|
+
_IngestHiddenAssetsOptions,
|
|
630
638
|
description=(
|
|
631
639
|
"When enabled, hidden worksheets and dashboards are ingested into Datahub."
|
|
632
640
|
" If a dashboard or worksheet is hidden in Tableau the luid is blank."
|
|
@@ -648,6 +656,11 @@ class TableauConfig(
|
|
|
648
656
|
# pre = True because we want to take some decision before pydantic initialize the configuration to default values
|
|
649
657
|
@root_validator(pre=True)
|
|
650
658
|
def projects_backward_compatibility(cls, values: Dict) -> Dict:
|
|
659
|
+
# In-place update of the input dict would cause state contamination. This was discovered through test failures
|
|
660
|
+
# in test_hex.py where the same dict is reused.
|
|
661
|
+
# So a copy is performed first.
|
|
662
|
+
values = deepcopy(values)
|
|
663
|
+
|
|
651
664
|
projects = values.get("projects")
|
|
652
665
|
project_pattern = values.get("project_pattern")
|
|
653
666
|
project_path_pattern = values.get("project_path_pattern")
|
|
@@ -659,6 +672,7 @@ class TableauConfig(
|
|
|
659
672
|
values["project_pattern"] = AllowDenyPattern(
|
|
660
673
|
allow=[f"^{prj}$" for prj in projects]
|
|
661
674
|
)
|
|
675
|
+
values.pop("projects")
|
|
662
676
|
elif (project_pattern or project_path_pattern) and projects:
|
|
663
677
|
raise ValueError(
|
|
664
678
|
"projects is deprecated. Please use project_path_pattern only."
|
|
@@ -670,7 +684,7 @@ class TableauConfig(
|
|
|
670
684
|
|
|
671
685
|
return values
|
|
672
686
|
|
|
673
|
-
@root_validator()
|
|
687
|
+
@root_validator(skip_on_failure=True)
|
|
674
688
|
def validate_config_values(cls, values: Dict) -> Dict:
|
|
675
689
|
tags_for_hidden_assets = values.get("tags_for_hidden_assets")
|
|
676
690
|
ingest_tags = values.get("ingest_tags")
|
|
@@ -8,7 +8,12 @@ import pydantic
|
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
from typing_extensions import Literal
|
|
10
10
|
|
|
11
|
-
from datahub.configuration.common import
|
|
11
|
+
from datahub.configuration.common import (
|
|
12
|
+
AllowDenyPattern,
|
|
13
|
+
ConfigEnum,
|
|
14
|
+
ConfigModel,
|
|
15
|
+
HiddenFromDocs,
|
|
16
|
+
)
|
|
12
17
|
from datahub.configuration.source_common import (
|
|
13
18
|
DatasetSourceConfigMixin,
|
|
14
19
|
LowerCaseDatasetUrnConfigMixin,
|
|
@@ -285,10 +290,9 @@ class UnityCatalogSourceConfig(
|
|
|
285
290
|
description="Limit the number of columns to get column level lineage. ",
|
|
286
291
|
)
|
|
287
292
|
|
|
288
|
-
lineage_max_workers: int = pydantic.Field(
|
|
293
|
+
lineage_max_workers: HiddenFromDocs[int] = pydantic.Field(
|
|
289
294
|
default=5 * (os.cpu_count() or 4),
|
|
290
295
|
description="Number of worker threads to use for column lineage thread pool executor. Set to 1 to disable.",
|
|
291
|
-
hidden_from_docs=True,
|
|
292
296
|
)
|
|
293
297
|
|
|
294
298
|
databricks_api_page_size: int = pydantic.Field(
|
|
@@ -18,7 +18,7 @@ import pydantic
|
|
|
18
18
|
from pydantic.fields import Field
|
|
19
19
|
|
|
20
20
|
import datahub.emitter.mce_builder as builder
|
|
21
|
-
from datahub.configuration.common import AllowDenyPattern
|
|
21
|
+
from datahub.configuration.common import AllowDenyPattern, HiddenFromDocs
|
|
22
22
|
from datahub.configuration.time_window_config import (
|
|
23
23
|
BaseTimeWindowConfig,
|
|
24
24
|
BucketDuration,
|
|
@@ -194,13 +194,13 @@ class GenericAggregatedDataset(Generic[ResourceType]):
|
|
|
194
194
|
|
|
195
195
|
|
|
196
196
|
class BaseUsageConfig(BaseTimeWindowConfig):
|
|
197
|
-
queries_character_limit: int = Field(
|
|
197
|
+
queries_character_limit: HiddenFromDocs[int] = Field(
|
|
198
|
+
# Hidden since we don't want to encourage people to break elasticsearch.
|
|
198
199
|
default=DEFAULT_QUERIES_CHARACTER_LIMIT,
|
|
199
200
|
description=(
|
|
200
201
|
"Total character limit for all queries in a single usage aspect."
|
|
201
202
|
" Queries will be truncated to length `queries_character_limit / top_n_queries`."
|
|
202
203
|
),
|
|
203
|
-
hidden_from_docs=True, # Don't want to encourage people to break elasticsearch
|
|
204
204
|
)
|
|
205
205
|
|
|
206
206
|
top_n_queries: pydantic.PositiveInt = Field(
|
|
@@ -2,6 +2,7 @@ import re
|
|
|
2
2
|
from typing import Dict, List, Optional, Union
|
|
3
3
|
from urllib.parse import urlparse
|
|
4
4
|
|
|
5
|
+
import pydantic
|
|
5
6
|
from pydantic import Field, validator
|
|
6
7
|
|
|
7
8
|
from datahub.configuration.common import AllowDenyPattern
|
|
@@ -121,7 +122,8 @@ class PulsarSourceConfig(
|
|
|
121
122
|
)
|
|
122
123
|
return client_secret
|
|
123
124
|
|
|
124
|
-
@
|
|
125
|
+
@pydantic.field_validator("web_service_url", mode="after")
|
|
126
|
+
@classmethod
|
|
125
127
|
def web_service_url_scheme_host_port(cls, val: str) -> str:
|
|
126
128
|
# Tokenize the web url
|
|
127
129
|
url = urlparse(val)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Dict, List, Optional, cast
|
|
4
|
+
|
|
5
|
+
from datahub.configuration.common import (
|
|
6
|
+
TransformerSemanticsConfigModel,
|
|
7
|
+
)
|
|
8
|
+
from datahub.emitter.mce_builder import Aspect
|
|
9
|
+
from datahub.ingestion.api.common import PipelineContext
|
|
10
|
+
from datahub.ingestion.transformer.base_transformer import (
|
|
11
|
+
BaseTransformer,
|
|
12
|
+
SingleAspectTransformer,
|
|
13
|
+
)
|
|
14
|
+
from datahub.metadata.schema_classes import (
|
|
15
|
+
BrowsePathEntryClass,
|
|
16
|
+
BrowsePathsV2Class,
|
|
17
|
+
)
|
|
18
|
+
from datahub.utilities.urns.urn import guess_entity_type
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SetBrowsePathTransformerConfig(TransformerSemanticsConfigModel):
|
|
22
|
+
path: List[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SetBrowsePathTransformer(BaseTransformer, SingleAspectTransformer):
|
|
26
|
+
ctx: PipelineContext
|
|
27
|
+
config: SetBrowsePathTransformerConfig
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: SetBrowsePathTransformerConfig, ctx: PipelineContext):
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.ctx = ctx
|
|
32
|
+
self.config = config
|
|
33
|
+
|
|
34
|
+
def aspect_name(self) -> str:
|
|
35
|
+
return "browsePathsV2"
|
|
36
|
+
|
|
37
|
+
def entity_types(self) -> List[str]:
|
|
38
|
+
# This is an arbitrary list, might be adjusted if it makes sense. It might be reasonable to make it configurable
|
|
39
|
+
return ["dataset", "dataJob", "dataFlow", "chart", "dashboard", "container"]
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create(
|
|
43
|
+
cls, config_dict: dict, ctx: PipelineContext
|
|
44
|
+
) -> "SetBrowsePathTransformer":
|
|
45
|
+
config = SetBrowsePathTransformerConfig.parse_obj(config_dict)
|
|
46
|
+
return cls(config, ctx)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def _build_model(existing_browse_paths: BrowsePathsV2Class) -> Dict[str, List[str]]:
|
|
50
|
+
template_vars: Dict[str, List[str]] = {}
|
|
51
|
+
model: Dict[str, List[str]] = defaultdict(list)
|
|
52
|
+
for entry in existing_browse_paths.path or []:
|
|
53
|
+
if entry.urn:
|
|
54
|
+
entity_type = guess_entity_type(entry.urn)
|
|
55
|
+
model[entity_type].append(entry.urn)
|
|
56
|
+
|
|
57
|
+
for entity_type, urns in model.items():
|
|
58
|
+
template_vars[f"{entity_type}[*]"] = urns
|
|
59
|
+
for i, urn in enumerate(urns):
|
|
60
|
+
template_vars[f"{entity_type}[{i}]"] = [urn]
|
|
61
|
+
|
|
62
|
+
return template_vars
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def _expand_nodes(
|
|
66
|
+
cls, templates: List[str], template_vars: Dict[str, List[str]]
|
|
67
|
+
) -> BrowsePathsV2Class:
|
|
68
|
+
expanded_nodes: List[str] = []
|
|
69
|
+
for node in templates:
|
|
70
|
+
resolved_nodes = cls._resolve_template_to_nodes(node, template_vars)
|
|
71
|
+
expanded_nodes.extend(resolved_nodes)
|
|
72
|
+
|
|
73
|
+
processed_entries: List[BrowsePathEntryClass] = []
|
|
74
|
+
for node in expanded_nodes:
|
|
75
|
+
if not node or node.isspace():
|
|
76
|
+
continue
|
|
77
|
+
processed_entries.append(
|
|
78
|
+
BrowsePathEntryClass(
|
|
79
|
+
id=node, urn=node if node.startswith("urn:") else None
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
return BrowsePathsV2Class(path=processed_entries)
|
|
83
|
+
|
|
84
|
+
def transform_aspect(
|
|
85
|
+
self, entity_urn: str, aspect_name: str, aspect: Optional[Aspect]
|
|
86
|
+
) -> Optional[Aspect]:
|
|
87
|
+
template_vars: Dict[str, List[str]] = {}
|
|
88
|
+
if aspect is not None:
|
|
89
|
+
assert isinstance(aspect, BrowsePathsV2Class)
|
|
90
|
+
template_vars = self._build_model(aspect)
|
|
91
|
+
new_browse_paths: BrowsePathsV2Class = self._expand_nodes(
|
|
92
|
+
self.config.path, template_vars
|
|
93
|
+
)
|
|
94
|
+
if aspect is not None and not self.config.replace_existing:
|
|
95
|
+
for node in aspect.path:
|
|
96
|
+
new_browse_paths.path.append(node)
|
|
97
|
+
|
|
98
|
+
return cast(Aspect, new_browse_paths)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _resolve_template_to_nodes(
|
|
102
|
+
template_str: str, template_vars: Dict[str, List[str]]
|
|
103
|
+
) -> List[str]:
|
|
104
|
+
# This mechanism can be made simpler (match against known variables only) or more complex (e.g. by using a
|
|
105
|
+
# proper templating engine, like jinja).
|
|
106
|
+
template_str = template_str.strip()
|
|
107
|
+
var_pattern = re.findall(r"^\$([a-zA-Z]+\[[0-9*]+]$)", template_str)
|
|
108
|
+
|
|
109
|
+
if not var_pattern:
|
|
110
|
+
return [template_str]
|
|
111
|
+
|
|
112
|
+
return template_vars.get(var_pattern[0], [])
|
datahub/sdk/_shared.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
from typing import (
|
|
6
7
|
TYPE_CHECKING,
|
|
@@ -61,6 +62,7 @@ DataPlatformInstanceUrnOrStr: TypeAlias = Union[str, DataPlatformInstanceUrn]
|
|
|
61
62
|
DataPlatformUrnOrStr: TypeAlias = Union[str, DataPlatformUrn]
|
|
62
63
|
|
|
63
64
|
ActorUrn: TypeAlias = Union[CorpUserUrn, CorpGroupUrn]
|
|
65
|
+
ActorUrnOrStr: TypeAlias = Union[str, ActorUrn]
|
|
64
66
|
StructuredPropertyUrnOrStr: TypeAlias = Union[str, StructuredPropertyUrn]
|
|
65
67
|
StructuredPropertyValueType: TypeAlias = Union[str, float, int]
|
|
66
68
|
StructuredPropertyInputType: TypeAlias = Dict[
|
|
@@ -110,6 +112,130 @@ def parse_time_stamp(ts: Optional[models.TimeStampClass]) -> Optional[datetime]:
|
|
|
110
112
|
return parse_ts_millis(ts.time)
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
class ChangeAuditStampsMixin(ABC):
|
|
116
|
+
"""Mixin class for managing audit stamps on entities."""
|
|
117
|
+
|
|
118
|
+
__slots__ = ()
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def _get_audit_stamps(self) -> models.ChangeAuditStampsClass:
|
|
122
|
+
"""Get the audit stamps from the entity properties."""
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
@abstractmethod
|
|
126
|
+
def _set_audit_stamps(self, audit_stamps: models.ChangeAuditStampsClass) -> None:
|
|
127
|
+
"""Set the audit stamps on the entity properties."""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def last_modified(self) -> Optional[datetime]:
|
|
132
|
+
"""Get the last modification timestamp from audit stamps."""
|
|
133
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
134
|
+
if audit_stamps.lastModified.time == 0:
|
|
135
|
+
return None
|
|
136
|
+
return datetime.fromtimestamp(
|
|
137
|
+
audit_stamps.lastModified.time / 1000
|
|
138
|
+
) # supports only seconds precision
|
|
139
|
+
|
|
140
|
+
def set_last_modified(self, last_modified: datetime) -> None:
|
|
141
|
+
"""Set the last modification timestamp in audit stamps."""
|
|
142
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
143
|
+
audit_stamps.lastModified.time = make_ts_millis(last_modified)
|
|
144
|
+
self._set_audit_stamps(audit_stamps)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def last_modified_by(self) -> Optional[str]:
|
|
148
|
+
"""Get the last modification actor from audit stamps."""
|
|
149
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
150
|
+
if audit_stamps.lastModified.actor == builder.UNKNOWN_USER:
|
|
151
|
+
return None
|
|
152
|
+
return audit_stamps.lastModified.actor
|
|
153
|
+
|
|
154
|
+
def set_last_modified_by(self, last_modified_by: ActorUrnOrStr) -> None:
|
|
155
|
+
"""Set the last modification actor in audit stamps."""
|
|
156
|
+
if isinstance(last_modified_by, str):
|
|
157
|
+
last_modified_by = make_user_urn(last_modified_by)
|
|
158
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
159
|
+
audit_stamps.lastModified.actor = str(last_modified_by)
|
|
160
|
+
self._set_audit_stamps(audit_stamps)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def created_at(self) -> Optional[datetime]:
|
|
164
|
+
"""Get the creation timestamp from audit stamps."""
|
|
165
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
166
|
+
if audit_stamps.created.time == 0:
|
|
167
|
+
return None
|
|
168
|
+
return datetime.fromtimestamp(
|
|
169
|
+
audit_stamps.created.time / 1000
|
|
170
|
+
) # supports only seconds precision
|
|
171
|
+
|
|
172
|
+
def set_created_at(self, created_at: datetime) -> None:
|
|
173
|
+
"""Set the creation timestamp in audit stamps."""
|
|
174
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
175
|
+
audit_stamps.created.time = make_ts_millis(created_at)
|
|
176
|
+
self._set_audit_stamps(audit_stamps)
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def created_by(self) -> Optional[ActorUrnOrStr]:
|
|
180
|
+
"""Get the creation actor from audit stamps."""
|
|
181
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
182
|
+
if audit_stamps.created.actor == builder.UNKNOWN_USER:
|
|
183
|
+
return None
|
|
184
|
+
return audit_stamps.created.actor
|
|
185
|
+
|
|
186
|
+
def set_created_by(self, created_by: ActorUrnOrStr) -> None:
|
|
187
|
+
"""Set the creation actor in audit stamps."""
|
|
188
|
+
if isinstance(created_by, str):
|
|
189
|
+
created_by = make_user_urn(created_by)
|
|
190
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
191
|
+
audit_stamps.created.actor = str(created_by)
|
|
192
|
+
self._set_audit_stamps(audit_stamps)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def deleted_on(self) -> Optional[datetime]:
|
|
196
|
+
"""Get the deletion timestamp from audit stamps."""
|
|
197
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
198
|
+
if audit_stamps.deleted is None or audit_stamps.deleted.time == 0:
|
|
199
|
+
return None
|
|
200
|
+
return datetime.fromtimestamp(
|
|
201
|
+
audit_stamps.deleted.time / 1000
|
|
202
|
+
) # supports only seconds precision
|
|
203
|
+
|
|
204
|
+
def set_deleted_on(self, deleted_on: datetime) -> None:
|
|
205
|
+
"""Set the deletion timestamp in audit stamps."""
|
|
206
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
207
|
+
# Default constructor sets deleted to None
|
|
208
|
+
if audit_stamps.deleted is None:
|
|
209
|
+
audit_stamps.deleted = models.AuditStampClass(
|
|
210
|
+
time=0, actor=builder.UNKNOWN_USER
|
|
211
|
+
)
|
|
212
|
+
audit_stamps.deleted.time = make_ts_millis(deleted_on)
|
|
213
|
+
self._set_audit_stamps(audit_stamps)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def deleted_by(self) -> Optional[ActorUrnOrStr]:
|
|
217
|
+
"""Get the deletion actor from audit stamps."""
|
|
218
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
219
|
+
if (
|
|
220
|
+
audit_stamps.deleted is None
|
|
221
|
+
or audit_stamps.deleted.actor == builder.UNKNOWN_USER
|
|
222
|
+
):
|
|
223
|
+
return None
|
|
224
|
+
return audit_stamps.deleted.actor
|
|
225
|
+
|
|
226
|
+
def set_deleted_by(self, deleted_by: ActorUrnOrStr) -> None:
|
|
227
|
+
"""Set the deletion actor in audit stamps."""
|
|
228
|
+
if isinstance(deleted_by, str):
|
|
229
|
+
deleted_by = make_user_urn(deleted_by)
|
|
230
|
+
audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps()
|
|
231
|
+
if audit_stamps.deleted is None:
|
|
232
|
+
audit_stamps.deleted = models.AuditStampClass(
|
|
233
|
+
time=0, actor=builder.UNKNOWN_USER
|
|
234
|
+
)
|
|
235
|
+
audit_stamps.deleted.actor = str(deleted_by)
|
|
236
|
+
self._set_audit_stamps(audit_stamps)
|
|
237
|
+
|
|
238
|
+
|
|
113
239
|
class HasPlatformInstance(Entity):
|
|
114
240
|
__slots__ = ()
|
|
115
241
|
|
datahub/sdk/chart.py
CHANGED
|
@@ -10,6 +10,8 @@ import datahub.metadata.schema_classes as models
|
|
|
10
10
|
from datahub.emitter.enum_helpers import get_enum_options
|
|
11
11
|
from datahub.metadata.urns import ChartUrn, DatasetUrn, Urn
|
|
12
12
|
from datahub.sdk._shared import (
|
|
13
|
+
ActorUrnOrStr,
|
|
14
|
+
ChangeAuditStampsMixin,
|
|
13
15
|
DataPlatformInstanceUrnOrStr,
|
|
14
16
|
DataPlatformUrnOrStr,
|
|
15
17
|
DatasetUrnOrStr,
|
|
@@ -34,6 +36,7 @@ from datahub.utilities.sentinels import Unset, unset
|
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
class Chart(
|
|
39
|
+
ChangeAuditStampsMixin,
|
|
37
40
|
HasPlatformInstance,
|
|
38
41
|
HasSubtype,
|
|
39
42
|
HasOwnership,
|
|
@@ -70,6 +73,11 @@ class Chart(
|
|
|
70
73
|
chart_url: Optional[str] = None,
|
|
71
74
|
custom_properties: Optional[Dict[str, str]] = None,
|
|
72
75
|
last_modified: Optional[datetime] = None,
|
|
76
|
+
last_modified_by: Optional[ActorUrnOrStr] = None,
|
|
77
|
+
created_at: Optional[datetime] = None,
|
|
78
|
+
created_by: Optional[ActorUrnOrStr] = None,
|
|
79
|
+
deleted_on: Optional[datetime] = None,
|
|
80
|
+
deleted_by: Optional[ActorUrnOrStr] = None,
|
|
73
81
|
last_refreshed: Optional[datetime] = None,
|
|
74
82
|
chart_type: Optional[Union[str, models.ChartTypeClass]] = None,
|
|
75
83
|
access: Optional[str] = None,
|
|
@@ -94,13 +102,60 @@ class Chart(
|
|
|
94
102
|
self._set_extra_aspects(extra_aspects)
|
|
95
103
|
|
|
96
104
|
self._set_platform_instance(platform, platform_instance)
|
|
97
|
-
|
|
98
105
|
self._ensure_chart_props(display_name=display_name)
|
|
106
|
+
self._init_chart_properties(
|
|
107
|
+
description,
|
|
108
|
+
display_name,
|
|
109
|
+
external_url,
|
|
110
|
+
chart_url,
|
|
111
|
+
custom_properties,
|
|
112
|
+
last_modified,
|
|
113
|
+
last_modified_by,
|
|
114
|
+
created_at,
|
|
115
|
+
created_by,
|
|
116
|
+
last_refreshed,
|
|
117
|
+
deleted_on,
|
|
118
|
+
deleted_by,
|
|
119
|
+
chart_type,
|
|
120
|
+
access,
|
|
121
|
+
input_datasets,
|
|
122
|
+
)
|
|
123
|
+
self._init_standard_aspects(
|
|
124
|
+
parent_container, subtype, owners, links, tags, terms, domain
|
|
125
|
+
)
|
|
99
126
|
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
@classmethod
|
|
128
|
+
def _new_from_graph(cls, urn: Urn, current_aspects: models.AspectBag) -> Self:
|
|
129
|
+
assert isinstance(urn, ChartUrn)
|
|
130
|
+
entity = cls(
|
|
131
|
+
platform=urn.dashboard_tool,
|
|
132
|
+
name=urn.chart_id,
|
|
133
|
+
)
|
|
134
|
+
return entity._init_from_graph(current_aspects)
|
|
135
|
+
|
|
136
|
+
def _init_chart_properties(
|
|
137
|
+
self,
|
|
138
|
+
description: Optional[str],
|
|
139
|
+
display_name: Optional[str],
|
|
140
|
+
external_url: Optional[str],
|
|
141
|
+
chart_url: Optional[str],
|
|
142
|
+
custom_properties: Optional[Dict[str, str]],
|
|
143
|
+
last_modified: Optional[datetime],
|
|
144
|
+
last_modified_by: Optional[ActorUrnOrStr],
|
|
145
|
+
created_at: Optional[datetime],
|
|
146
|
+
created_by: Optional[ActorUrnOrStr],
|
|
147
|
+
last_refreshed: Optional[datetime],
|
|
148
|
+
deleted_on: Optional[datetime],
|
|
149
|
+
deleted_by: Optional[ActorUrnOrStr],
|
|
150
|
+
chart_type: Optional[Union[str, models.ChartTypeClass]],
|
|
151
|
+
access: Optional[str],
|
|
152
|
+
input_datasets: Optional[Sequence[Union[DatasetUrnOrStr, Dataset]]],
|
|
153
|
+
) -> None:
|
|
154
|
+
"""Initialize chart-specific properties."""
|
|
102
155
|
if description is not None:
|
|
103
156
|
self.set_description(description)
|
|
157
|
+
if display_name is not None:
|
|
158
|
+
self.set_display_name(display_name)
|
|
104
159
|
if external_url is not None:
|
|
105
160
|
self.set_external_url(external_url)
|
|
106
161
|
if chart_url is not None:
|
|
@@ -109,6 +164,16 @@ class Chart(
|
|
|
109
164
|
self.set_custom_properties(custom_properties)
|
|
110
165
|
if last_modified is not None:
|
|
111
166
|
self.set_last_modified(last_modified)
|
|
167
|
+
if last_modified_by is not None:
|
|
168
|
+
self.set_last_modified_by(last_modified_by)
|
|
169
|
+
if created_at is not None:
|
|
170
|
+
self.set_created_at(created_at)
|
|
171
|
+
if created_by is not None:
|
|
172
|
+
self.set_created_by(created_by)
|
|
173
|
+
if deleted_on is not None:
|
|
174
|
+
self.set_deleted_on(deleted_on)
|
|
175
|
+
if deleted_by is not None:
|
|
176
|
+
self.set_deleted_by(deleted_by)
|
|
112
177
|
if last_refreshed is not None:
|
|
113
178
|
self.set_last_refreshed(last_refreshed)
|
|
114
179
|
if chart_type is not None:
|
|
@@ -118,6 +183,17 @@ class Chart(
|
|
|
118
183
|
if input_datasets is not None:
|
|
119
184
|
self.set_input_datasets(input_datasets)
|
|
120
185
|
|
|
186
|
+
def _init_standard_aspects(
|
|
187
|
+
self,
|
|
188
|
+
parent_container: ParentContainerInputType | Unset,
|
|
189
|
+
subtype: Optional[str],
|
|
190
|
+
owners: Optional[OwnersInputType],
|
|
191
|
+
links: Optional[LinksInputType],
|
|
192
|
+
tags: Optional[TagsInputType],
|
|
193
|
+
terms: Optional[TermsInputType],
|
|
194
|
+
domain: Optional[DomainInputType],
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Initialize standard aspects."""
|
|
121
197
|
if parent_container is not unset:
|
|
122
198
|
self._set_container(parent_container)
|
|
123
199
|
if subtype is not None:
|
|
@@ -133,15 +209,6 @@ class Chart(
|
|
|
133
209
|
if domain is not None:
|
|
134
210
|
self.set_domain(domain)
|
|
135
211
|
|
|
136
|
-
@classmethod
|
|
137
|
-
def _new_from_graph(cls, urn: Urn, current_aspects: models.AspectBag) -> Self:
|
|
138
|
-
assert isinstance(urn, ChartUrn)
|
|
139
|
-
entity = cls(
|
|
140
|
-
platform=urn.dashboard_tool,
|
|
141
|
-
name=urn.chart_id,
|
|
142
|
-
)
|
|
143
|
-
return entity._init_from_graph(current_aspects)
|
|
144
|
-
|
|
145
212
|
@property
|
|
146
213
|
def urn(self) -> ChartUrn:
|
|
147
214
|
assert isinstance(self._urn, ChartUrn)
|
|
@@ -159,6 +226,14 @@ class Chart(
|
|
|
159
226
|
)
|
|
160
227
|
)
|
|
161
228
|
|
|
229
|
+
def _get_audit_stamps(self) -> models.ChangeAuditStampsClass:
|
|
230
|
+
"""Get the audit stamps from the chart properties."""
|
|
231
|
+
return self._ensure_chart_props().lastModified
|
|
232
|
+
|
|
233
|
+
def _set_audit_stamps(self, audit_stamps: models.ChangeAuditStampsClass) -> None:
|
|
234
|
+
"""Set the audit stamps on the chart properties."""
|
|
235
|
+
self._ensure_chart_props().lastModified = audit_stamps
|
|
236
|
+
|
|
162
237
|
@property
|
|
163
238
|
def name(self) -> str:
|
|
164
239
|
"""Get the name of the chart."""
|
|
@@ -220,24 +295,6 @@ class Chart(
|
|
|
220
295
|
"""Set the custom properties of the chart."""
|
|
221
296
|
self._ensure_chart_props().customProperties = custom_properties
|
|
222
297
|
|
|
223
|
-
@property
|
|
224
|
-
def last_modified(self) -> Optional[datetime]:
|
|
225
|
-
"""Get the last modification timestamp of the chart."""
|
|
226
|
-
last_modified_time = self._ensure_chart_props().lastModified.lastModified.time
|
|
227
|
-
if not last_modified_time:
|
|
228
|
-
return None
|
|
229
|
-
return datetime.fromtimestamp(last_modified_time)
|
|
230
|
-
|
|
231
|
-
def set_last_modified(self, last_modified: datetime) -> None:
|
|
232
|
-
"""Set the last modification timestamp of the chart."""
|
|
233
|
-
chart_props = self._ensure_chart_props()
|
|
234
|
-
chart_props.lastModified = models.ChangeAuditStampsClass(
|
|
235
|
-
lastModified=models.AuditStampClass(
|
|
236
|
-
time=int(last_modified.timestamp()),
|
|
237
|
-
actor="urn:li:corpuser:datahub",
|
|
238
|
-
)
|
|
239
|
-
)
|
|
240
|
-
|
|
241
298
|
@property
|
|
242
299
|
def last_refreshed(self) -> Optional[datetime]:
|
|
243
300
|
"""Get the last refresh timestamp of the chart."""
|