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.

Files changed (94) hide show
  1. {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/METADATA +2513 -2571
  2. {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/RECORD +94 -87
  3. {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/entry_points.txt +2 -0
  4. datahub/_version.py +1 -1
  5. datahub/api/entities/assertion/assertion.py +1 -1
  6. datahub/api/entities/corpgroup/corpgroup.py +1 -1
  7. datahub/api/entities/dataproduct/dataproduct.py +6 -3
  8. datahub/api/entities/dataset/dataset.py +9 -18
  9. datahub/api/entities/structuredproperties/structuredproperties.py +2 -2
  10. datahub/api/graphql/operation.py +10 -6
  11. datahub/cli/docker_check.py +2 -2
  12. datahub/configuration/common.py +29 -1
  13. datahub/configuration/connection_resolver.py +5 -2
  14. datahub/configuration/import_resolver.py +7 -4
  15. datahub/configuration/pydantic_migration_helpers.py +0 -9
  16. datahub/configuration/source_common.py +3 -2
  17. datahub/configuration/validate_field_deprecation.py +5 -2
  18. datahub/configuration/validate_field_removal.py +5 -2
  19. datahub/configuration/validate_field_rename.py +6 -5
  20. datahub/configuration/validate_multiline_string.py +5 -2
  21. datahub/ingestion/autogenerated/capability_summary.json +33 -1
  22. datahub/ingestion/run/pipeline_config.py +2 -2
  23. datahub/ingestion/source/azure/azure_common.py +1 -1
  24. datahub/ingestion/source/bigquery_v2/bigquery_config.py +28 -14
  25. datahub/ingestion/source/bigquery_v2/queries_extractor.py +4 -5
  26. datahub/ingestion/source/common/gcp_credentials_config.py +3 -1
  27. datahub/ingestion/source/data_lake_common/path_spec.py +16 -16
  28. datahub/ingestion/source/datahub/config.py +8 -9
  29. datahub/ingestion/source/delta_lake/config.py +1 -1
  30. datahub/ingestion/source/dremio/dremio_config.py +3 -4
  31. datahub/ingestion/source/feast.py +8 -10
  32. datahub/ingestion/source/fivetran/config.py +1 -1
  33. datahub/ingestion/source/ge_profiling_config.py +26 -22
  34. datahub/ingestion/source/grafana/grafana_config.py +2 -2
  35. datahub/ingestion/source/grafana/models.py +12 -14
  36. datahub/ingestion/source/hex/hex.py +6 -1
  37. datahub/ingestion/source/iceberg/iceberg_profiler.py +4 -2
  38. datahub/ingestion/source/kafka_connect/common.py +2 -2
  39. datahub/ingestion/source/looker/looker_common.py +1 -1
  40. datahub/ingestion/source/looker/looker_config.py +15 -4
  41. datahub/ingestion/source/looker/looker_source.py +52 -3
  42. datahub/ingestion/source/looker/lookml_config.py +1 -1
  43. datahub/ingestion/source/metadata/business_glossary.py +7 -7
  44. datahub/ingestion/source/metadata/lineage.py +1 -1
  45. datahub/ingestion/source/mode.py +13 -5
  46. datahub/ingestion/source/nifi.py +1 -1
  47. datahub/ingestion/source/powerbi/config.py +14 -21
  48. datahub/ingestion/source/preset.py +1 -1
  49. datahub/ingestion/source/qlik_sense/data_classes.py +28 -8
  50. datahub/ingestion/source/redshift/config.py +6 -3
  51. datahub/ingestion/source/salesforce.py +13 -9
  52. datahub/ingestion/source/schema/json_schema.py +14 -14
  53. datahub/ingestion/source/sigma/data_classes.py +3 -0
  54. datahub/ingestion/source/snaplogic/__init__.py +0 -0
  55. datahub/ingestion/source/snaplogic/snaplogic.py +355 -0
  56. datahub/ingestion/source/snaplogic/snaplogic_config.py +37 -0
  57. datahub/ingestion/source/snaplogic/snaplogic_lineage_extractor.py +107 -0
  58. datahub/ingestion/source/snaplogic/snaplogic_parser.py +168 -0
  59. datahub/ingestion/source/snaplogic/snaplogic_utils.py +31 -0
  60. datahub/ingestion/source/snowflake/snowflake_config.py +12 -15
  61. datahub/ingestion/source/snowflake/snowflake_connection.py +8 -3
  62. datahub/ingestion/source/snowflake/snowflake_lineage_v2.py +15 -2
  63. datahub/ingestion/source/snowflake/snowflake_queries.py +4 -5
  64. datahub/ingestion/source/sql/athena.py +2 -1
  65. datahub/ingestion/source/sql/clickhouse.py +12 -7
  66. datahub/ingestion/source/sql/cockroachdb.py +5 -3
  67. datahub/ingestion/source/sql/druid.py +2 -2
  68. datahub/ingestion/source/sql/hive.py +4 -3
  69. datahub/ingestion/source/sql/hive_metastore.py +7 -9
  70. datahub/ingestion/source/sql/mssql/source.py +2 -2
  71. datahub/ingestion/source/sql/mysql.py +2 -2
  72. datahub/ingestion/source/sql/oracle.py +3 -3
  73. datahub/ingestion/source/sql/presto.py +2 -1
  74. datahub/ingestion/source/sql/teradata.py +4 -4
  75. datahub/ingestion/source/sql/trino.py +2 -1
  76. datahub/ingestion/source/sql/two_tier_sql_source.py +2 -3
  77. datahub/ingestion/source/sql/vertica.py +1 -1
  78. datahub/ingestion/source/sql_queries.py +6 -6
  79. datahub/ingestion/source/state/checkpoint.py +5 -1
  80. datahub/ingestion/source/state/entity_removal_state.py +5 -2
  81. datahub/ingestion/source/state/stateful_ingestion_base.py +5 -8
  82. datahub/ingestion/source/superset.py +1 -2
  83. datahub/ingestion/source/tableau/tableau.py +20 -6
  84. datahub/ingestion/source/unity/config.py +7 -3
  85. datahub/ingestion/source/usage/usage_common.py +3 -3
  86. datahub/ingestion/source_config/pulsar.py +3 -1
  87. datahub/ingestion/transformer/set_browse_path.py +112 -0
  88. datahub/sdk/_shared.py +126 -0
  89. datahub/sdk/chart.py +87 -30
  90. datahub/sdk/dashboard.py +79 -32
  91. datahub/sdk/search_filters.py +1 -7
  92. {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/WHEEL +0 -0
  93. {acryl_datahub-1.2.0.10rc3.dist-info → acryl_datahub-1.2.0.10rc5.dist-info}/licenses/LICENSE +0 -0
  94. {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=False,
590
- description="[Experimental] Whether to extract lineage from unsupported custom sql queries using SQL parsing",
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 custom sql queries using SQL parsing, ignoring Tableau metadata",
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[Literal["worksheet", "dashboard"]], bool] = Field(
629
- default=["worksheet", "dashboard"],
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 AllowDenyPattern, ConfigEnum, ConfigModel
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
- @validator("web_service_url")
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
- if display_name is not None:
101
- self.set_display_name(display_name)
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."""