mirror of
				https://github.com/datahub-project/datahub.git
				synced 2025-10-31 02:37:05 +00:00 
			
		
		
		
	feat(sdk): Added support for Change Audit Stamps in Dashboard and Chart entities (#14815)
This commit is contained in:
		
							parent
							
								
									9be65bd971
								
							
						
					
					
						commit
						0f69e96078
					
				| @ -736,7 +736,16 @@ class LookerDashboardSource(TestableSource, StatefulIngestionSourceBase): | |||||||
|                 display_name=dashboard_element.title,  # title is (deprecated) using display_name |                 display_name=dashboard_element.title,  # title is (deprecated) using display_name | ||||||
|                 extra_aspects=chart_extra_aspects, |                 extra_aspects=chart_extra_aspects, | ||||||
|                 input_datasets=dashboard_element.get_view_urns(self.source_config), |                 input_datasets=dashboard_element.get_view_urns(self.source_config), | ||||||
|                 last_modified=self._get_last_modified_time(dashboard), |                 last_modified=self._get_last_modified_time( | ||||||
|  |                     dashboard | ||||||
|  |                 ),  # Inherited from Dashboard | ||||||
|  |                 last_modified_by=self._get_last_modified_by( | ||||||
|  |                     dashboard | ||||||
|  |                 ),  # Inherited from Dashboard | ||||||
|  |                 created_at=self._get_created_at(dashboard),  # Inherited from Dashboard | ||||||
|  |                 created_by=self._get_created_by(dashboard),  # Inherited from Dashboard | ||||||
|  |                 deleted_on=self._get_deleted_on(dashboard),  # Inherited from Dashboard | ||||||
|  |                 deleted_by=self._get_deleted_by(dashboard),  # Inherited from Dashboard | ||||||
|                 name=dashboard_element.get_urn_element_id(), |                 name=dashboard_element.get_urn_element_id(), | ||||||
|                 owners=chart_ownership, |                 owners=chart_ownership, | ||||||
|                 parent_container=chart_parent_container, |                 parent_container=chart_parent_container, | ||||||
| @ -803,6 +812,11 @@ class LookerDashboardSource(TestableSource, StatefulIngestionSourceBase): | |||||||
|                 display_name=looker_dashboard.title,  # title is (deprecated) using display_name |                 display_name=looker_dashboard.title,  # title is (deprecated) using display_name | ||||||
|                 extra_aspects=dashboard_extra_aspects, |                 extra_aspects=dashboard_extra_aspects, | ||||||
|                 last_modified=self._get_last_modified_time(looker_dashboard), |                 last_modified=self._get_last_modified_time(looker_dashboard), | ||||||
|  |                 last_modified_by=self._get_last_modified_by(looker_dashboard), | ||||||
|  |                 created_at=self._get_created_at(looker_dashboard), | ||||||
|  |                 created_by=self._get_created_by(looker_dashboard), | ||||||
|  |                 deleted_on=self._get_deleted_on(looker_dashboard), | ||||||
|  |                 deleted_by=self._get_deleted_by(looker_dashboard), | ||||||
|                 name=looker_dashboard.get_urn_dashboard_id(), |                 name=looker_dashboard.get_urn_dashboard_id(), | ||||||
|                 owners=dashboard_ownership, |                 owners=dashboard_ownership, | ||||||
|                 parent_container=dashboard_parent_container, |                 parent_container=dashboard_parent_container, | ||||||
| @ -988,9 +1002,44 @@ class LookerDashboardSource(TestableSource, StatefulIngestionSourceBase): | |||||||
|     def _get_last_modified_time( |     def _get_last_modified_time( | ||||||
|         self, looker_dashboard: Optional[LookerDashboard] |         self, looker_dashboard: Optional[LookerDashboard] | ||||||
|     ) -> Optional[datetime.datetime]: |     ) -> Optional[datetime.datetime]: | ||||||
|         if looker_dashboard is None: |         return looker_dashboard.last_updated_at if looker_dashboard else None | ||||||
|  | 
 | ||||||
|  |     def _get_last_modified_by( | ||||||
|  |         self, looker_dashboard: Optional[LookerDashboard] | ||||||
|  |     ) -> Optional[str]: | ||||||
|  |         if not looker_dashboard or not looker_dashboard.last_updated_by: | ||||||
|             return None |             return None | ||||||
|         return looker_dashboard.last_updated_at |         return looker_dashboard.last_updated_by.get_urn( | ||||||
|  |             self.source_config.strip_user_ids_from_email | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def _get_created_at( | ||||||
|  |         self, looker_dashboard: Optional[LookerDashboard] | ||||||
|  |     ) -> Optional[datetime.datetime]: | ||||||
|  |         return looker_dashboard.created_at if looker_dashboard else None | ||||||
|  | 
 | ||||||
|  |     def _get_created_by( | ||||||
|  |         self, looker_dashboard: Optional[LookerDashboard] | ||||||
|  |     ) -> Optional[str]: | ||||||
|  |         if not looker_dashboard or not looker_dashboard.owner: | ||||||
|  |             return None | ||||||
|  |         return looker_dashboard.owner.get_urn( | ||||||
|  |             self.source_config.strip_user_ids_from_email | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def _get_deleted_on( | ||||||
|  |         self, looker_dashboard: Optional[LookerDashboard] | ||||||
|  |     ) -> Optional[datetime.datetime]: | ||||||
|  |         return looker_dashboard.deleted_at if looker_dashboard else None | ||||||
|  | 
 | ||||||
|  |     def _get_deleted_by( | ||||||
|  |         self, looker_dashboard: Optional[LookerDashboard] | ||||||
|  |     ) -> Optional[str]: | ||||||
|  |         if not looker_dashboard or not looker_dashboard.deleted_by: | ||||||
|  |             return None | ||||||
|  |         return looker_dashboard.deleted_by.get_urn( | ||||||
|  |             self.source_config.strip_user_ids_from_email | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     def _get_looker_folder(self, folder: Union[Folder, FolderBase]) -> LookerFolder: |     def _get_looker_folder(self, folder: Union[Folder, FolderBase]) -> LookerFolder: | ||||||
|         assert folder.id |         assert folder.id | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import warnings | import warnings | ||||||
|  | from abc import ABC, abstractmethod | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from typing import ( | from typing import ( | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
| @ -61,6 +62,7 @@ DataPlatformInstanceUrnOrStr: TypeAlias = Union[str, DataPlatformInstanceUrn] | |||||||
| DataPlatformUrnOrStr: TypeAlias = Union[str, DataPlatformUrn] | DataPlatformUrnOrStr: TypeAlias = Union[str, DataPlatformUrn] | ||||||
| 
 | 
 | ||||||
| ActorUrn: TypeAlias = Union[CorpUserUrn, CorpGroupUrn] | ActorUrn: TypeAlias = Union[CorpUserUrn, CorpGroupUrn] | ||||||
|  | ActorUrnOrStr: TypeAlias = Union[str, ActorUrn] | ||||||
| StructuredPropertyUrnOrStr: TypeAlias = Union[str, StructuredPropertyUrn] | StructuredPropertyUrnOrStr: TypeAlias = Union[str, StructuredPropertyUrn] | ||||||
| StructuredPropertyValueType: TypeAlias = Union[str, float, int] | StructuredPropertyValueType: TypeAlias = Union[str, float, int] | ||||||
| StructuredPropertyInputType: TypeAlias = Dict[ | StructuredPropertyInputType: TypeAlias = Dict[ | ||||||
| @ -110,6 +112,130 @@ def parse_time_stamp(ts: Optional[models.TimeStampClass]) -> Optional[datetime]: | |||||||
|     return parse_ts_millis(ts.time) |     return parse_ts_millis(ts.time) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class ChangeAuditStampsMixin(ABC): | ||||||
|  |     """Mixin class for managing audit stamps on entities.""" | ||||||
|  | 
 | ||||||
|  |     __slots__ = () | ||||||
|  | 
 | ||||||
|  |     @abstractmethod | ||||||
|  |     def _get_audit_stamps(self) -> models.ChangeAuditStampsClass: | ||||||
|  |         """Get the audit stamps from the entity properties.""" | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     @abstractmethod | ||||||
|  |     def _set_audit_stamps(self, audit_stamps: models.ChangeAuditStampsClass) -> None: | ||||||
|  |         """Set the audit stamps on the entity properties.""" | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def last_modified(self) -> Optional[datetime]: | ||||||
|  |         """Get the last modification timestamp from audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         if audit_stamps.lastModified.time == 0: | ||||||
|  |             return None | ||||||
|  |         return datetime.fromtimestamp( | ||||||
|  |             audit_stamps.lastModified.time / 1000 | ||||||
|  |         )  # supports only seconds precision | ||||||
|  | 
 | ||||||
|  |     def set_last_modified(self, last_modified: datetime) -> None: | ||||||
|  |         """Set the last modification timestamp in audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         audit_stamps.lastModified.time = make_ts_millis(last_modified) | ||||||
|  |         self._set_audit_stamps(audit_stamps) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def last_modified_by(self) -> Optional[str]: | ||||||
|  |         """Get the last modification actor from audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         if audit_stamps.lastModified.actor == builder.UNKNOWN_USER: | ||||||
|  |             return None | ||||||
|  |         return audit_stamps.lastModified.actor | ||||||
|  | 
 | ||||||
|  |     def set_last_modified_by(self, last_modified_by: ActorUrnOrStr) -> None: | ||||||
|  |         """Set the last modification actor in audit stamps.""" | ||||||
|  |         if isinstance(last_modified_by, str): | ||||||
|  |             last_modified_by = make_user_urn(last_modified_by) | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         audit_stamps.lastModified.actor = str(last_modified_by) | ||||||
|  |         self._set_audit_stamps(audit_stamps) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def created_at(self) -> Optional[datetime]: | ||||||
|  |         """Get the creation timestamp from audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         if audit_stamps.created.time == 0: | ||||||
|  |             return None | ||||||
|  |         return datetime.fromtimestamp( | ||||||
|  |             audit_stamps.created.time / 1000 | ||||||
|  |         )  # supports only seconds precision | ||||||
|  | 
 | ||||||
|  |     def set_created_at(self, created_at: datetime) -> None: | ||||||
|  |         """Set the creation timestamp in audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         audit_stamps.created.time = make_ts_millis(created_at) | ||||||
|  |         self._set_audit_stamps(audit_stamps) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def created_by(self) -> Optional[ActorUrnOrStr]: | ||||||
|  |         """Get the creation actor from audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         if audit_stamps.created.actor == builder.UNKNOWN_USER: | ||||||
|  |             return None | ||||||
|  |         return audit_stamps.created.actor | ||||||
|  | 
 | ||||||
|  |     def set_created_by(self, created_by: ActorUrnOrStr) -> None: | ||||||
|  |         """Set the creation actor in audit stamps.""" | ||||||
|  |         if isinstance(created_by, str): | ||||||
|  |             created_by = make_user_urn(created_by) | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         audit_stamps.created.actor = str(created_by) | ||||||
|  |         self._set_audit_stamps(audit_stamps) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def deleted_on(self) -> Optional[datetime]: | ||||||
|  |         """Get the deletion timestamp from audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         if audit_stamps.deleted is None or audit_stamps.deleted.time == 0: | ||||||
|  |             return None | ||||||
|  |         return datetime.fromtimestamp( | ||||||
|  |             audit_stamps.deleted.time / 1000 | ||||||
|  |         )  # supports only seconds precision | ||||||
|  | 
 | ||||||
|  |     def set_deleted_on(self, deleted_on: datetime) -> None: | ||||||
|  |         """Set the deletion timestamp in audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         # Default constructor sets deleted to None | ||||||
|  |         if audit_stamps.deleted is None: | ||||||
|  |             audit_stamps.deleted = models.AuditStampClass( | ||||||
|  |                 time=0, actor=builder.UNKNOWN_USER | ||||||
|  |             ) | ||||||
|  |         audit_stamps.deleted.time = make_ts_millis(deleted_on) | ||||||
|  |         self._set_audit_stamps(audit_stamps) | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def deleted_by(self) -> Optional[ActorUrnOrStr]: | ||||||
|  |         """Get the deletion actor from audit stamps.""" | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         if ( | ||||||
|  |             audit_stamps.deleted is None | ||||||
|  |             or audit_stamps.deleted.actor == builder.UNKNOWN_USER | ||||||
|  |         ): | ||||||
|  |             return None | ||||||
|  |         return audit_stamps.deleted.actor | ||||||
|  | 
 | ||||||
|  |     def set_deleted_by(self, deleted_by: ActorUrnOrStr) -> None: | ||||||
|  |         """Set the deletion actor in audit stamps.""" | ||||||
|  |         if isinstance(deleted_by, str): | ||||||
|  |             deleted_by = make_user_urn(deleted_by) | ||||||
|  |         audit_stamps: models.ChangeAuditStampsClass = self._get_audit_stamps() | ||||||
|  |         if audit_stamps.deleted is None: | ||||||
|  |             audit_stamps.deleted = models.AuditStampClass( | ||||||
|  |                 time=0, actor=builder.UNKNOWN_USER | ||||||
|  |             ) | ||||||
|  |         audit_stamps.deleted.actor = str(deleted_by) | ||||||
|  |         self._set_audit_stamps(audit_stamps) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class HasPlatformInstance(Entity): | class HasPlatformInstance(Entity): | ||||||
|     __slots__ = () |     __slots__ = () | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -10,6 +10,8 @@ import datahub.metadata.schema_classes as models | |||||||
| from datahub.emitter.enum_helpers import get_enum_options | from datahub.emitter.enum_helpers import get_enum_options | ||||||
| from datahub.metadata.urns import ChartUrn, DatasetUrn, Urn | from datahub.metadata.urns import ChartUrn, DatasetUrn, Urn | ||||||
| from datahub.sdk._shared import ( | from datahub.sdk._shared import ( | ||||||
|  |     ActorUrnOrStr, | ||||||
|  |     ChangeAuditStampsMixin, | ||||||
|     DataPlatformInstanceUrnOrStr, |     DataPlatformInstanceUrnOrStr, | ||||||
|     DataPlatformUrnOrStr, |     DataPlatformUrnOrStr, | ||||||
|     DatasetUrnOrStr, |     DatasetUrnOrStr, | ||||||
| @ -34,6 +36,7 @@ from datahub.utilities.sentinels import Unset, unset | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Chart( | class Chart( | ||||||
|  |     ChangeAuditStampsMixin, | ||||||
|     HasPlatformInstance, |     HasPlatformInstance, | ||||||
|     HasSubtype, |     HasSubtype, | ||||||
|     HasOwnership, |     HasOwnership, | ||||||
| @ -70,6 +73,11 @@ class Chart( | |||||||
|         chart_url: Optional[str] = None, |         chart_url: Optional[str] = None, | ||||||
|         custom_properties: Optional[Dict[str, str]] = None, |         custom_properties: Optional[Dict[str, str]] = None, | ||||||
|         last_modified: Optional[datetime] = None, |         last_modified: Optional[datetime] = None, | ||||||
|  |         last_modified_by: Optional[ActorUrnOrStr] = None, | ||||||
|  |         created_at: Optional[datetime] = None, | ||||||
|  |         created_by: Optional[ActorUrnOrStr] = None, | ||||||
|  |         deleted_on: Optional[datetime] = None, | ||||||
|  |         deleted_by: Optional[ActorUrnOrStr] = None, | ||||||
|         last_refreshed: Optional[datetime] = None, |         last_refreshed: Optional[datetime] = None, | ||||||
|         chart_type: Optional[Union[str, models.ChartTypeClass]] = None, |         chart_type: Optional[Union[str, models.ChartTypeClass]] = None, | ||||||
|         access: Optional[str] = None, |         access: Optional[str] = None, | ||||||
| @ -94,13 +102,60 @@ class Chart( | |||||||
|         self._set_extra_aspects(extra_aspects) |         self._set_extra_aspects(extra_aspects) | ||||||
| 
 | 
 | ||||||
|         self._set_platform_instance(platform, platform_instance) |         self._set_platform_instance(platform, platform_instance) | ||||||
| 
 |  | ||||||
|         self._ensure_chart_props(display_name=display_name) |         self._ensure_chart_props(display_name=display_name) | ||||||
|  |         self._init_chart_properties( | ||||||
|  |             description, | ||||||
|  |             display_name, | ||||||
|  |             external_url, | ||||||
|  |             chart_url, | ||||||
|  |             custom_properties, | ||||||
|  |             last_modified, | ||||||
|  |             last_modified_by, | ||||||
|  |             created_at, | ||||||
|  |             created_by, | ||||||
|  |             last_refreshed, | ||||||
|  |             deleted_on, | ||||||
|  |             deleted_by, | ||||||
|  |             chart_type, | ||||||
|  |             access, | ||||||
|  |             input_datasets, | ||||||
|  |         ) | ||||||
|  |         self._init_standard_aspects( | ||||||
|  |             parent_container, subtype, owners, links, tags, terms, domain | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         if display_name is not None: |     @classmethod | ||||||
|             self.set_display_name(display_name) |     def _new_from_graph(cls, urn: Urn, current_aspects: models.AspectBag) -> Self: | ||||||
|  |         assert isinstance(urn, ChartUrn) | ||||||
|  |         entity = cls( | ||||||
|  |             platform=urn.dashboard_tool, | ||||||
|  |             name=urn.chart_id, | ||||||
|  |         ) | ||||||
|  |         return entity._init_from_graph(current_aspects) | ||||||
|  | 
 | ||||||
|  |     def _init_chart_properties( | ||||||
|  |         self, | ||||||
|  |         description: Optional[str], | ||||||
|  |         display_name: Optional[str], | ||||||
|  |         external_url: Optional[str], | ||||||
|  |         chart_url: Optional[str], | ||||||
|  |         custom_properties: Optional[Dict[str, str]], | ||||||
|  |         last_modified: Optional[datetime], | ||||||
|  |         last_modified_by: Optional[ActorUrnOrStr], | ||||||
|  |         created_at: Optional[datetime], | ||||||
|  |         created_by: Optional[ActorUrnOrStr], | ||||||
|  |         last_refreshed: Optional[datetime], | ||||||
|  |         deleted_on: Optional[datetime], | ||||||
|  |         deleted_by: Optional[ActorUrnOrStr], | ||||||
|  |         chart_type: Optional[Union[str, models.ChartTypeClass]], | ||||||
|  |         access: Optional[str], | ||||||
|  |         input_datasets: Optional[Sequence[Union[DatasetUrnOrStr, Dataset]]], | ||||||
|  |     ) -> None: | ||||||
|  |         """Initialize chart-specific properties.""" | ||||||
|         if description is not None: |         if description is not None: | ||||||
|             self.set_description(description) |             self.set_description(description) | ||||||
|  |         if display_name is not None: | ||||||
|  |             self.set_display_name(display_name) | ||||||
|         if external_url is not None: |         if external_url is not None: | ||||||
|             self.set_external_url(external_url) |             self.set_external_url(external_url) | ||||||
|         if chart_url is not None: |         if chart_url is not None: | ||||||
| @ -109,6 +164,16 @@ class Chart( | |||||||
|             self.set_custom_properties(custom_properties) |             self.set_custom_properties(custom_properties) | ||||||
|         if last_modified is not None: |         if last_modified is not None: | ||||||
|             self.set_last_modified(last_modified) |             self.set_last_modified(last_modified) | ||||||
|  |         if last_modified_by is not None: | ||||||
|  |             self.set_last_modified_by(last_modified_by) | ||||||
|  |         if created_at is not None: | ||||||
|  |             self.set_created_at(created_at) | ||||||
|  |         if created_by is not None: | ||||||
|  |             self.set_created_by(created_by) | ||||||
|  |         if deleted_on is not None: | ||||||
|  |             self.set_deleted_on(deleted_on) | ||||||
|  |         if deleted_by is not None: | ||||||
|  |             self.set_deleted_by(deleted_by) | ||||||
|         if last_refreshed is not None: |         if last_refreshed is not None: | ||||||
|             self.set_last_refreshed(last_refreshed) |             self.set_last_refreshed(last_refreshed) | ||||||
|         if chart_type is not None: |         if chart_type is not None: | ||||||
| @ -118,6 +183,17 @@ class Chart( | |||||||
|         if input_datasets is not None: |         if input_datasets is not None: | ||||||
|             self.set_input_datasets(input_datasets) |             self.set_input_datasets(input_datasets) | ||||||
| 
 | 
 | ||||||
|  |     def _init_standard_aspects( | ||||||
|  |         self, | ||||||
|  |         parent_container: ParentContainerInputType | Unset, | ||||||
|  |         subtype: Optional[str], | ||||||
|  |         owners: Optional[OwnersInputType], | ||||||
|  |         links: Optional[LinksInputType], | ||||||
|  |         tags: Optional[TagsInputType], | ||||||
|  |         terms: Optional[TermsInputType], | ||||||
|  |         domain: Optional[DomainInputType], | ||||||
|  |     ) -> None: | ||||||
|  |         """Initialize standard aspects.""" | ||||||
|         if parent_container is not unset: |         if parent_container is not unset: | ||||||
|             self._set_container(parent_container) |             self._set_container(parent_container) | ||||||
|         if subtype is not None: |         if subtype is not None: | ||||||
| @ -133,15 +209,6 @@ class Chart( | |||||||
|         if domain is not None: |         if domain is not None: | ||||||
|             self.set_domain(domain) |             self.set_domain(domain) | ||||||
| 
 | 
 | ||||||
|     @classmethod |  | ||||||
|     def _new_from_graph(cls, urn: Urn, current_aspects: models.AspectBag) -> Self: |  | ||||||
|         assert isinstance(urn, ChartUrn) |  | ||||||
|         entity = cls( |  | ||||||
|             platform=urn.dashboard_tool, |  | ||||||
|             name=urn.chart_id, |  | ||||||
|         ) |  | ||||||
|         return entity._init_from_graph(current_aspects) |  | ||||||
| 
 |  | ||||||
|     @property |     @property | ||||||
|     def urn(self) -> ChartUrn: |     def urn(self) -> ChartUrn: | ||||||
|         assert isinstance(self._urn, ChartUrn) |         assert isinstance(self._urn, ChartUrn) | ||||||
| @ -159,6 +226,14 @@ class Chart( | |||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |     def _get_audit_stamps(self) -> models.ChangeAuditStampsClass: | ||||||
|  |         """Get the audit stamps from the chart properties.""" | ||||||
|  |         return self._ensure_chart_props().lastModified | ||||||
|  | 
 | ||||||
|  |     def _set_audit_stamps(self, audit_stamps: models.ChangeAuditStampsClass) -> None: | ||||||
|  |         """Set the audit stamps on the chart properties.""" | ||||||
|  |         self._ensure_chart_props().lastModified = audit_stamps | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def name(self) -> str: |     def name(self) -> str: | ||||||
|         """Get the name of the chart.""" |         """Get the name of the chart.""" | ||||||
| @ -220,24 +295,6 @@ class Chart( | |||||||
|         """Set the custom properties of the chart.""" |         """Set the custom properties of the chart.""" | ||||||
|         self._ensure_chart_props().customProperties = custom_properties |         self._ensure_chart_props().customProperties = custom_properties | ||||||
| 
 | 
 | ||||||
|     @property |  | ||||||
|     def last_modified(self) -> Optional[datetime]: |  | ||||||
|         """Get the last modification timestamp of the chart.""" |  | ||||||
|         last_modified_time = self._ensure_chart_props().lastModified.lastModified.time |  | ||||||
|         if not last_modified_time: |  | ||||||
|             return None |  | ||||||
|         return datetime.fromtimestamp(last_modified_time) |  | ||||||
| 
 |  | ||||||
|     def set_last_modified(self, last_modified: datetime) -> None: |  | ||||||
|         """Set the last modification timestamp of the chart.""" |  | ||||||
|         chart_props = self._ensure_chart_props() |  | ||||||
|         chart_props.lastModified = models.ChangeAuditStampsClass( |  | ||||||
|             lastModified=models.AuditStampClass( |  | ||||||
|                 time=int(last_modified.timestamp()), |  | ||||||
|                 actor="urn:li:corpuser:datahub", |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     @property |     @property | ||||||
|     def last_refreshed(self) -> Optional[datetime]: |     def last_refreshed(self) -> Optional[datetime]: | ||||||
|         """Get the last refresh timestamp of the chart.""" |         """Get the last refresh timestamp of the chart.""" | ||||||
|  | |||||||
| @ -9,6 +9,8 @@ from typing_extensions import Self | |||||||
| import datahub.metadata.schema_classes as models | import datahub.metadata.schema_classes as models | ||||||
| from datahub.metadata.urns import ChartUrn, DashboardUrn, DatasetUrn, Urn | from datahub.metadata.urns import ChartUrn, DashboardUrn, DatasetUrn, Urn | ||||||
| from datahub.sdk._shared import ( | from datahub.sdk._shared import ( | ||||||
|  |     ActorUrnOrStr, | ||||||
|  |     ChangeAuditStampsMixin, | ||||||
|     ChartUrnOrStr, |     ChartUrnOrStr, | ||||||
|     DashboardUrnOrStr, |     DashboardUrnOrStr, | ||||||
|     DataPlatformInstanceUrnOrStr, |     DataPlatformInstanceUrnOrStr, | ||||||
| @ -36,6 +38,7 @@ from datahub.utilities.sentinels import Unset, unset | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Dashboard( | class Dashboard( | ||||||
|  |     ChangeAuditStampsMixin, | ||||||
|     HasPlatformInstance, |     HasPlatformInstance, | ||||||
|     HasSubtype, |     HasSubtype, | ||||||
|     HasOwnership, |     HasOwnership, | ||||||
| @ -72,6 +75,11 @@ class Dashboard( | |||||||
|         dashboard_url: Optional[str] = None, |         dashboard_url: Optional[str] = None, | ||||||
|         custom_properties: Optional[Dict[str, str]] = None, |         custom_properties: Optional[Dict[str, str]] = None, | ||||||
|         last_modified: Optional[datetime] = None, |         last_modified: Optional[datetime] = None, | ||||||
|  |         last_modified_by: Optional[ActorUrnOrStr] = None, | ||||||
|  |         created_at: Optional[datetime] = None, | ||||||
|  |         created_by: Optional[ActorUrnOrStr] = None, | ||||||
|  |         deleted_on: Optional[datetime] = None, | ||||||
|  |         deleted_by: Optional[ActorUrnOrStr] = None, | ||||||
|         last_refreshed: Optional[datetime] = None, |         last_refreshed: Optional[datetime] = None, | ||||||
|         input_datasets: Optional[Sequence[Union[DatasetUrnOrStr, Dataset]]] = None, |         input_datasets: Optional[Sequence[Union[DatasetUrnOrStr, Dataset]]] = None, | ||||||
|         charts: Optional[Sequence[Union[ChartUrnOrStr, Chart]]] = None, |         charts: Optional[Sequence[Union[ChartUrnOrStr, Chart]]] = None, | ||||||
| @ -96,17 +104,48 @@ class Dashboard( | |||||||
|         self._set_extra_aspects(extra_aspects) |         self._set_extra_aspects(extra_aspects) | ||||||
| 
 | 
 | ||||||
|         self._set_platform_instance(platform, platform_instance) |         self._set_platform_instance(platform, platform_instance) | ||||||
|  |         self._ensure_dashboard_props(display_name=display_name) | ||||||
| 
 | 
 | ||||||
|         # Initialize DashboardInfoClass with default values |         self._init_dashboard_properties( | ||||||
|         dashboard_info = self._ensure_dashboard_props(display_name=display_name) |             description, | ||||||
|         if last_modified: |             display_name, | ||||||
|             dashboard_info.lastModified = models.ChangeAuditStampsClass( |             external_url, | ||||||
|                 lastModified=models.AuditStampClass( |             dashboard_url, | ||||||
|                     time=int(last_modified.timestamp()), |             custom_properties, | ||||||
|                     actor="urn:li:corpuser:datahub", |             last_modified, | ||||||
|                 ), |             last_modified_by, | ||||||
|             ) |             created_at, | ||||||
|  |             created_by, | ||||||
|  |             last_refreshed, | ||||||
|  |             deleted_on, | ||||||
|  |             deleted_by, | ||||||
|  |             input_datasets, | ||||||
|  |             charts, | ||||||
|  |             dashboards, | ||||||
|  |         ) | ||||||
|  |         self._init_standard_aspects( | ||||||
|  |             parent_container, subtype, owners, links, tags, terms, domain | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|  |     def _init_dashboard_properties( | ||||||
|  |         self, | ||||||
|  |         description: Optional[str], | ||||||
|  |         display_name: Optional[str], | ||||||
|  |         external_url: Optional[str], | ||||||
|  |         dashboard_url: Optional[str], | ||||||
|  |         custom_properties: Optional[Dict[str, str]], | ||||||
|  |         last_modified: Optional[datetime], | ||||||
|  |         last_modified_by: Optional[ActorUrnOrStr], | ||||||
|  |         created_at: Optional[datetime], | ||||||
|  |         created_by: Optional[ActorUrnOrStr], | ||||||
|  |         last_refreshed: Optional[datetime], | ||||||
|  |         deleted_on: Optional[datetime], | ||||||
|  |         deleted_by: Optional[ActorUrnOrStr], | ||||||
|  |         input_datasets: Optional[Sequence[Union[DatasetUrnOrStr, Dataset]]], | ||||||
|  |         charts: Optional[Sequence[Union[ChartUrnOrStr, Chart]]], | ||||||
|  |         dashboards: Optional[Sequence[Union[DashboardUrnOrStr, Dashboard]]], | ||||||
|  |     ) -> None: | ||||||
|  |         """Initialize dashboard-specific properties.""" | ||||||
|         if description is not None: |         if description is not None: | ||||||
|             self.set_description(description) |             self.set_description(description) | ||||||
|         if display_name is not None: |         if display_name is not None: | ||||||
| @ -119,6 +158,16 @@ class Dashboard( | |||||||
|             self.set_custom_properties(custom_properties) |             self.set_custom_properties(custom_properties) | ||||||
|         if last_modified is not None: |         if last_modified is not None: | ||||||
|             self.set_last_modified(last_modified) |             self.set_last_modified(last_modified) | ||||||
|  |         if last_modified_by is not None: | ||||||
|  |             self.set_last_modified_by(last_modified_by) | ||||||
|  |         if created_at is not None: | ||||||
|  |             self.set_created_at(created_at) | ||||||
|  |         if created_by is not None: | ||||||
|  |             self.set_created_by(created_by) | ||||||
|  |         if deleted_on is not None: | ||||||
|  |             self.set_deleted_on(deleted_on) | ||||||
|  |         if deleted_by is not None: | ||||||
|  |             self.set_deleted_by(deleted_by) | ||||||
|         if last_refreshed is not None: |         if last_refreshed is not None: | ||||||
|             self.set_last_refreshed(last_refreshed) |             self.set_last_refreshed(last_refreshed) | ||||||
|         if input_datasets is not None: |         if input_datasets is not None: | ||||||
| @ -128,6 +177,17 @@ class Dashboard( | |||||||
|         if dashboards is not None: |         if dashboards is not None: | ||||||
|             self.set_dashboards(dashboards) |             self.set_dashboards(dashboards) | ||||||
| 
 | 
 | ||||||
|  |     def _init_standard_aspects( | ||||||
|  |         self, | ||||||
|  |         parent_container: ParentContainerInputType | Unset, | ||||||
|  |         subtype: Optional[str], | ||||||
|  |         owners: Optional[OwnersInputType], | ||||||
|  |         links: Optional[LinksInputType], | ||||||
|  |         tags: Optional[TagsInputType], | ||||||
|  |         terms: Optional[TermsInputType], | ||||||
|  |         domain: Optional[DomainInputType], | ||||||
|  |     ) -> None: | ||||||
|  |         """Initialize standard aspects.""" | ||||||
|         if parent_container is not unset: |         if parent_container is not unset: | ||||||
|             self._set_container(parent_container) |             self._set_container(parent_container) | ||||||
|         if subtype is not None: |         if subtype is not None: | ||||||
| @ -165,16 +225,20 @@ class Dashboard( | |||||||
|             models.DashboardInfoClass( |             models.DashboardInfoClass( | ||||||
|                 title=display_name or self.urn.dashboard_id, |                 title=display_name or self.urn.dashboard_id, | ||||||
|                 description="", |                 description="", | ||||||
|                 lastModified=models.ChangeAuditStampsClass( |                 lastModified=models.ChangeAuditStampsClass(), | ||||||
|                     lastModified=models.AuditStampClass( |  | ||||||
|                         time=0, actor="urn:li:corpuser:unknown" |  | ||||||
|                     ) |  | ||||||
|                 ), |  | ||||||
|                 customProperties={}, |                 customProperties={}, | ||||||
|                 dashboards=[], |                 dashboards=[], | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |     def _get_audit_stamps(self) -> models.ChangeAuditStampsClass: | ||||||
|  |         """Get the audit stamps from the dashboard properties.""" | ||||||
|  |         return self._ensure_dashboard_props().lastModified | ||||||
|  | 
 | ||||||
|  |     def _set_audit_stamps(self, audit_stamps: models.ChangeAuditStampsClass) -> None: | ||||||
|  |         """Set the audit stamps on the dashboard properties.""" | ||||||
|  |         self._ensure_dashboard_props().lastModified = audit_stamps | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def name(self) -> str: |     def name(self) -> str: | ||||||
|         """Get the name of the dashboard.""" |         """Get the name of the dashboard.""" | ||||||
| @ -238,23 +302,6 @@ class Dashboard( | |||||||
|         """Set the custom properties of the dashboard.""" |         """Set the custom properties of the dashboard.""" | ||||||
|         self._ensure_dashboard_props().customProperties = custom_properties |         self._ensure_dashboard_props().customProperties = custom_properties | ||||||
| 
 | 
 | ||||||
|     @property |  | ||||||
|     def last_modified(self) -> Optional[datetime]: |  | ||||||
|         """Get the last modification timestamp of the dashboard.""" |  | ||||||
|         props = self._ensure_dashboard_props() |  | ||||||
|         if props.lastModified.lastModified.time == 0: |  | ||||||
|             return None |  | ||||||
|         return datetime.fromtimestamp(props.lastModified.lastModified.time) |  | ||||||
| 
 |  | ||||||
|     def set_last_modified(self, last_modified: datetime) -> None: |  | ||||||
|         """Set the last modification timestamp of the dashboard.""" |  | ||||||
|         self._ensure_dashboard_props().lastModified = models.ChangeAuditStampsClass( |  | ||||||
|             lastModified=models.AuditStampClass( |  | ||||||
|                 time=int(last_modified.timestamp()), |  | ||||||
|                 actor="urn:li:corpuser:datahub", |  | ||||||
|             ), |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     @property |     @property | ||||||
|     def last_refreshed(self) -> Optional[datetime]: |     def last_refreshed(self) -> Optional[datetime]: | ||||||
|         """Get the last refresh timestamp of the dashboard.""" |         """Get the last refresh timestamp of the dashboard.""" | ||||||
|  | |||||||
| @ -0,0 +1,92 @@ | |||||||
|  | [ | ||||||
|  | { | ||||||
|  |     "entityType": "chart", | ||||||
|  |     "entityUrn": "urn:li:chart:(looker,audit_golden_chart)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "dataPlatformInstance", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "platform": "urn:li:dataPlatform:looker" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }, | ||||||
|  | { | ||||||
|  |     "entityType": "chart", | ||||||
|  |     "entityUrn": "urn:li:chart:(looker,audit_golden_chart)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "chartInfo", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "customProperties": { | ||||||
|  |                 "audit_test": "true", | ||||||
|  |                 "chart_type": "line" | ||||||
|  |             }, | ||||||
|  |             "title": "Audit Golden Chart", | ||||||
|  |             "description": "Testing chart audit stamps with golden files", | ||||||
|  |             "lastModified": { | ||||||
|  |                 "created": { | ||||||
|  |                     "time": 1672596000000, | ||||||
|  |                     "actor": "urn:li:corpuser:creator@example.com" | ||||||
|  |                 }, | ||||||
|  |                 "lastModified": { | ||||||
|  |                     "time": 1672698600000, | ||||||
|  |                     "actor": "urn:li:corpuser:modifier@example.com" | ||||||
|  |                 }, | ||||||
|  |                 "deleted": { | ||||||
|  |                     "time": 1672793100000, | ||||||
|  |                     "actor": "urn:li:corpuser:deleter@example.com" | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             "inputs": [ | ||||||
|  |                 { | ||||||
|  |                     "string": "urn:li:dataset:(urn:li:dataPlatform:snowflake,sales_data,PROD)" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "string": "urn:li:dataset:(urn:li:dataPlatform:snowflake,user_data,PROD)" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "type": "LINE", | ||||||
|  |             "access": "PRIVATE" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }, | ||||||
|  | { | ||||||
|  |     "entityType": "chart", | ||||||
|  |     "entityUrn": "urn:li:chart:(looker,audit_golden_chart)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "ownership", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "owners": [ | ||||||
|  |                 { | ||||||
|  |                     "owner": "urn:li:corpuser:owner@example.com", | ||||||
|  |                     "type": "TECHNICAL_OWNER" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "ownerTypes": {}, | ||||||
|  |             "lastModified": { | ||||||
|  |                 "time": 0, | ||||||
|  |                 "actor": "urn:li:corpuser:unknown" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }, | ||||||
|  | { | ||||||
|  |     "entityType": "chart", | ||||||
|  |     "entityUrn": "urn:li:chart:(looker,audit_golden_chart)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "globalTags", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "tags": [ | ||||||
|  |                 { | ||||||
|  |                     "tag": "urn:li:tag:audit" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "tag": "urn:li:tag:chart" | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ] | ||||||
| @ -0,0 +1,84 @@ | |||||||
|  | [ | ||||||
|  | { | ||||||
|  |     "entityType": "dashboard", | ||||||
|  |     "entityUrn": "urn:li:dashboard:(looker,audit_golden_dashboard)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "dataPlatformInstance", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "platform": "urn:li:dataPlatform:looker" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }, | ||||||
|  | { | ||||||
|  |     "entityType": "dashboard", | ||||||
|  |     "entityUrn": "urn:li:dashboard:(looker,audit_golden_dashboard)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "dashboardInfo", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "customProperties": { | ||||||
|  |                 "audit_test": "true" | ||||||
|  |             }, | ||||||
|  |             "title": "Audit Golden Test", | ||||||
|  |             "description": "Testing audit stamps with golden files", | ||||||
|  |             "charts": [], | ||||||
|  |             "datasets": [], | ||||||
|  |             "dashboards": [], | ||||||
|  |             "lastModified": { | ||||||
|  |                 "created": { | ||||||
|  |                     "time": 1672596000000, | ||||||
|  |                     "actor": "urn:li:corpuser:creator@example.com" | ||||||
|  |                 }, | ||||||
|  |                 "lastModified": { | ||||||
|  |                     "time": 1672698600000, | ||||||
|  |                     "actor": "urn:li:corpuser:modifier@example.com" | ||||||
|  |                 }, | ||||||
|  |                 "deleted": { | ||||||
|  |                     "time": 1672793100000, | ||||||
|  |                     "actor": "urn:li:corpuser:deleter@example.com" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }, | ||||||
|  | { | ||||||
|  |     "entityType": "dashboard", | ||||||
|  |     "entityUrn": "urn:li:dashboard:(looker,audit_golden_dashboard)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "ownership", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "owners": [ | ||||||
|  |                 { | ||||||
|  |                     "owner": "urn:li:corpuser:owner@example.com", | ||||||
|  |                     "type": "TECHNICAL_OWNER" | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             "ownerTypes": {}, | ||||||
|  |             "lastModified": { | ||||||
|  |                 "time": 0, | ||||||
|  |                 "actor": "urn:li:corpuser:unknown" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }, | ||||||
|  | { | ||||||
|  |     "entityType": "dashboard", | ||||||
|  |     "entityUrn": "urn:li:dashboard:(looker,audit_golden_dashboard)", | ||||||
|  |     "changeType": "UPSERT", | ||||||
|  |     "aspectName": "globalTags", | ||||||
|  |     "aspect": { | ||||||
|  |         "json": { | ||||||
|  |             "tags": [ | ||||||
|  |                 { | ||||||
|  |                     "tag": "urn:li:tag:audit" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     "tag": "urn:li:tag:test" | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | ] | ||||||
| @ -243,3 +243,131 @@ def test_chart_set_chart_type() -> None: | |||||||
|     assert c.chart_type == models.ChartTypeClass.BAR |     assert c.chart_type == models.ChartTypeClass.BAR | ||||||
|     with pytest.raises(ValueError, match=r"Invalid chart type:.*"): |     with pytest.raises(ValueError, match=r"Invalid chart type:.*"): | ||||||
|         c.set_chart_type("invalid_type") |         c.set_chart_type("invalid_type") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_chart_audit_stamps_integration() -> None: | ||||||
|  |     """Test HasAuditStamps mixin integration with Chart-specific functionality.""" | ||||||
|  |     created_time = datetime(2023, 1, 1, 10, 0, 0) | ||||||
|  |     modified_time = datetime(2023, 1, 2, 14, 30, 0) | ||||||
|  |     deleted_time = datetime(2023, 1, 3, 16, 45, 0) | ||||||
|  | 
 | ||||||
|  |     # Test initialization with audit stamps and chart-specific properties | ||||||
|  |     c = Chart( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_integration_chart", | ||||||
|  |         display_name="Audit Integration Chart", | ||||||
|  |         description="Testing audit stamps with chart features", | ||||||
|  |         created_at=created_time, | ||||||
|  |         created_by="creator@example.com", | ||||||
|  |         last_modified=modified_time, | ||||||
|  |         last_modified_by="modifier@example.com", | ||||||
|  |         deleted_on=deleted_time, | ||||||
|  |         deleted_by="deleter@example.com", | ||||||
|  |         chart_type=models.ChartTypeClass.BAR, | ||||||
|  |         access=models.AccessLevelClass.PUBLIC, | ||||||
|  |         owners=[CorpUserUrn("owner@example.com")], | ||||||
|  |         tags=[TagUrn("test")], | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Verify audit stamps work alongside chart properties | ||||||
|  |     assert c.created_at == created_time | ||||||
|  |     assert c.created_by == "urn:li:corpuser:creator@example.com" | ||||||
|  |     assert c.last_modified == modified_time | ||||||
|  |     assert c.last_modified_by == "urn:li:corpuser:modifier@example.com" | ||||||
|  |     assert c.deleted_on == deleted_time | ||||||
|  |     assert c.deleted_by == "urn:li:corpuser:deleter@example.com" | ||||||
|  | 
 | ||||||
|  |     # Verify chart-specific properties still work | ||||||
|  |     assert c.display_name == "Audit Integration Chart" | ||||||
|  |     assert c.description == "Testing audit stamps with chart features" | ||||||
|  |     assert c.chart_type == models.ChartTypeClass.BAR | ||||||
|  |     assert c.access == models.AccessLevelClass.PUBLIC | ||||||
|  |     assert c.owners is not None | ||||||
|  |     assert c.tags is not None | ||||||
|  | 
 | ||||||
|  |     # Test that modifying audit stamps doesn't affect chart properties | ||||||
|  |     new_modified_time = datetime(2023, 1, 4, 18, 0, 0) | ||||||
|  |     c.set_last_modified(new_modified_time) | ||||||
|  |     c.set_last_modified_by("new_modifier@example.com") | ||||||
|  | 
 | ||||||
|  |     assert c.last_modified == new_modified_time | ||||||
|  |     assert c.last_modified_by == "urn:li:corpuser:new_modifier@example.com" | ||||||
|  | 
 | ||||||
|  |     # Chart properties should remain unchanged | ||||||
|  |     assert c.display_name == "Audit Integration Chart" | ||||||
|  |     assert c.chart_type == models.ChartTypeClass.BAR | ||||||
|  |     assert c.access == models.AccessLevelClass.PUBLIC | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_chart_audit_stamps_with_input_datasets() -> None: | ||||||
|  |     """Test audit stamps with chart input dataset operations.""" | ||||||
|  |     c = Chart( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_datasets_chart", | ||||||
|  |         created_at=datetime(2023, 1, 1, 10, 0, 0), | ||||||
|  |         created_by="creator@example.com", | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Add input datasets | ||||||
|  |     c.add_input_dataset("urn:li:dataset:(urn:li:dataPlatform:snowflake,table1,PROD)") | ||||||
|  |     c.add_input_dataset("urn:li:dataset:(urn:li:dataPlatform:snowflake,table2,PROD)") | ||||||
|  | 
 | ||||||
|  |     # Verify audit stamps and input datasets work together | ||||||
|  |     assert c.created_at == datetime(2023, 1, 1, 10, 0, 0) | ||||||
|  |     assert c.created_by == "urn:li:corpuser:creator@example.com" | ||||||
|  |     assert len(c.input_datasets) == 2 | ||||||
|  | 
 | ||||||
|  |     # Modify audit stamps | ||||||
|  |     c.set_last_modified(datetime(2023, 1, 2, 12, 0, 0)) | ||||||
|  |     c.set_last_modified_by("modifier@example.com") | ||||||
|  | 
 | ||||||
|  |     # Verify both audit stamps and input datasets are preserved | ||||||
|  |     assert c.last_modified == datetime(2023, 1, 2, 12, 0, 0) | ||||||
|  |     assert c.last_modified_by == "urn:li:corpuser:modifier@example.com" | ||||||
|  |     assert len(c.input_datasets) == 2 | ||||||
|  | 
 | ||||||
|  |     # Remove a dataset | ||||||
|  |     c.remove_input_dataset("urn:li:dataset:(urn:li:dataPlatform:snowflake,table1,PROD)") | ||||||
|  | 
 | ||||||
|  |     # Verify audit stamps are still intact | ||||||
|  |     assert c.last_modified == datetime(2023, 1, 2, 12, 0, 0) | ||||||
|  |     assert c.last_modified_by == "urn:li:corpuser:modifier@example.com" | ||||||
|  |     assert len(c.input_datasets) == 1 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_chart_audit_stamps_golden() -> None: | ||||||
|  |     """Test chart audit stamps with golden file comparison.""" | ||||||
|  |     created_time = datetime(2023, 1, 1, 10, 0, 0) | ||||||
|  |     modified_time = datetime(2023, 1, 2, 14, 30, 0) | ||||||
|  |     deleted_time = datetime(2023, 1, 3, 16, 45, 0) | ||||||
|  | 
 | ||||||
|  |     c = Chart( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_golden_chart", | ||||||
|  |         display_name="Audit Golden Chart", | ||||||
|  |         description="Testing chart audit stamps with golden files", | ||||||
|  |         created_at=created_time, | ||||||
|  |         created_by="creator@example.com", | ||||||
|  |         last_modified=modified_time, | ||||||
|  |         last_modified_by="modifier@example.com", | ||||||
|  |         deleted_on=deleted_time, | ||||||
|  |         deleted_by="deleter@example.com", | ||||||
|  |         chart_type=models.ChartTypeClass.LINE, | ||||||
|  |         access=models.AccessLevelClass.PRIVATE, | ||||||
|  |         owners=[CorpUserUrn("owner@example.com")], | ||||||
|  |         tags=[TagUrn("audit"), TagUrn("chart")], | ||||||
|  |         custom_properties={"audit_test": "true", "chart_type": "line"}, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Add some input datasets for comprehensive testing | ||||||
|  |     c.add_input_dataset( | ||||||
|  |         "urn:li:dataset:(urn:li:dataPlatform:snowflake,sales_data,PROD)" | ||||||
|  |     ) | ||||||
|  |     c.add_input_dataset("urn:li:dataset:(urn:li:dataPlatform:snowflake,user_data,PROD)") | ||||||
|  | 
 | ||||||
|  |     # Generate golden file for chart audit stamps functionality | ||||||
|  |     assert_entity_golden( | ||||||
|  |         c, | ||||||
|  |         GOLDEN_DIR / "test_chart_audit_stamps_golden.json", | ||||||
|  |         ["lastRefreshed"],  # Exclude timestamp fields that might vary between test runs | ||||||
|  |     ) | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ from unittest import mock | |||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
|  | from datahub.emitter import mce_builder | ||||||
| from datahub.errors import ItemNotFoundError | from datahub.errors import ItemNotFoundError | ||||||
| from datahub.metadata.urns import ( | from datahub.metadata.urns import ( | ||||||
|     ChartUrn, |     ChartUrn, | ||||||
| @ -227,3 +228,213 @@ def test_client_get_dashboard() -> None: | |||||||
|     mock_entities.get.side_effect = ItemNotFoundError(error_message) |     mock_entities.get.side_effect = ItemNotFoundError(error_message) | ||||||
|     with pytest.raises(ItemNotFoundError, match=re.escape(error_message)): |     with pytest.raises(ItemNotFoundError, match=re.escape(error_message)): | ||||||
|         mock_client.entities.get(dashboard_urn) |         mock_client.entities.get(dashboard_urn) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_dashboard_audit_stamps() -> None: | ||||||
|  |     """Test HasAuditStamps mixin functionality on Dashboard.""" | ||||||
|  |     created_time = datetime(2023, 1, 1, 10, 0, 0) | ||||||
|  |     modified_time = datetime(2023, 1, 2, 14, 30, 0) | ||||||
|  |     deleted_time = datetime(2023, 1, 3, 16, 45, 0) | ||||||
|  | 
 | ||||||
|  |     # Test initialization with audit stamps | ||||||
|  |     d = Dashboard( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_test_dashboard", | ||||||
|  |         created_at=created_time, | ||||||
|  |         created_by="creator@example.com", | ||||||
|  |         last_modified=modified_time, | ||||||
|  |         last_modified_by="modifier@example.com", | ||||||
|  |         deleted_on=deleted_time, | ||||||
|  |         deleted_by="deleter@example.com", | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Test created_at and created_by | ||||||
|  |     assert d.created_at == created_time | ||||||
|  |     assert d.created_by == "urn:li:corpuser:creator@example.com" | ||||||
|  | 
 | ||||||
|  |     # Test last_modified and last_modified_by | ||||||
|  |     assert d.last_modified == modified_time | ||||||
|  |     assert d.last_modified_by == "urn:li:corpuser:modifier@example.com" | ||||||
|  | 
 | ||||||
|  |     # Test deleted_on and deleted_by | ||||||
|  |     assert d.deleted_on == deleted_time | ||||||
|  |     assert d.deleted_by == "urn:li:corpuser:deleter@example.com" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_dashboard_audit_stamps_setters() -> None: | ||||||
|  |     """Test setting audit stamps after initialization.""" | ||||||
|  |     d = Dashboard( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_setter_test_dashboard", | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Initially all should be None | ||||||
|  |     assert d.created_at is None | ||||||
|  |     assert d.created_by is None | ||||||
|  |     assert d.last_modified is None | ||||||
|  |     assert d.last_modified_by is None | ||||||
|  |     assert d.deleted_on is None | ||||||
|  |     assert d.deleted_by is None | ||||||
|  | 
 | ||||||
|  |     # Test setting timestamps | ||||||
|  |     created_time = datetime(2023, 1, 1, 10, 0, 0) | ||||||
|  |     modified_time = datetime(2023, 1, 2, 14, 30, 0) | ||||||
|  |     deleted_time = datetime(2023, 1, 3, 16, 45, 0) | ||||||
|  | 
 | ||||||
|  |     d.set_created_at(created_time) | ||||||
|  |     d.set_last_modified(modified_time) | ||||||
|  |     d.set_deleted_on(deleted_time) | ||||||
|  | 
 | ||||||
|  |     assert d.created_at == created_time | ||||||
|  |     assert d.last_modified == modified_time | ||||||
|  |     assert d.deleted_on == deleted_time | ||||||
|  | 
 | ||||||
|  |     # Test setting actors with string URNs | ||||||
|  |     d.set_created_by("creator@example.com") | ||||||
|  |     d.set_last_modified_by("modifier@example.com") | ||||||
|  |     d.set_deleted_by("deleter@example.com") | ||||||
|  | 
 | ||||||
|  |     assert d.created_by == "urn:li:corpuser:creator@example.com" | ||||||
|  |     assert d.last_modified_by == "urn:li:corpuser:modifier@example.com" | ||||||
|  |     assert d.deleted_by == "urn:li:corpuser:deleter@example.com" | ||||||
|  | 
 | ||||||
|  |     # Test setting actors with CorpUserUrn objects | ||||||
|  |     from datahub.metadata.urns import CorpUserUrn | ||||||
|  | 
 | ||||||
|  |     creator_urn = CorpUserUrn("creator_urn@example.com") | ||||||
|  |     modifier_urn = CorpUserUrn("modifier_urn@example.com") | ||||||
|  |     deleter_urn = CorpUserUrn("deleter_urn@example.com") | ||||||
|  | 
 | ||||||
|  |     d.set_created_by(creator_urn) | ||||||
|  |     d.set_last_modified_by(modifier_urn) | ||||||
|  |     d.set_deleted_by(deleter_urn) | ||||||
|  | 
 | ||||||
|  |     assert d.created_by == str(creator_urn) | ||||||
|  |     assert d.last_modified_by == str(modifier_urn) | ||||||
|  |     assert d.deleted_by == str(deleter_urn) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_dashboard_audit_stamps_edge_cases() -> None: | ||||||
|  |     """Test edge cases for audit stamps - setting None values""" | ||||||
|  |     d = Dashboard( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_edge_case_dashboard", | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # These should not raise errors and should set to None or default values | ||||||
|  |     assert d.created_by is None | ||||||
|  |     assert d.last_modified_by is None | ||||||
|  |     assert d.deleted_by is None | ||||||
|  | 
 | ||||||
|  |     # Internally it should have the default values | ||||||
|  |     assert d._get_audit_stamps().created.actor == mce_builder.UNKNOWN_USER | ||||||
|  |     assert d._get_audit_stamps().lastModified.actor == mce_builder.UNKNOWN_USER | ||||||
|  |     assert ( | ||||||
|  |         d._get_audit_stamps().deleted is None | ||||||
|  |     )  # deleted has no default value as per the pdl | ||||||
|  | 
 | ||||||
|  |     assert d.created_at is None | ||||||
|  |     assert d.last_modified is None | ||||||
|  |     assert d.deleted_on is None | ||||||
|  | 
 | ||||||
|  |     # Internally it should have the default values | ||||||
|  |     assert d._get_audit_stamps().created.time == 0 | ||||||
|  |     assert d._get_audit_stamps().lastModified.time == 0 | ||||||
|  |     assert ( | ||||||
|  |         d._get_audit_stamps().deleted is None | ||||||
|  |     )  # deleted has no default value as per the pdl | ||||||
|  | 
 | ||||||
|  |     # Test that timestamps are properly converted | ||||||
|  |     test_time = datetime(2023, 1, 1, 12, 0, 0) | ||||||
|  |     d.set_created_at(test_time) | ||||||
|  |     d.set_last_modified(test_time) | ||||||
|  |     d.set_deleted_on(test_time) | ||||||
|  | 
 | ||||||
|  |     # Verify the timestamps are stored correctly | ||||||
|  |     assert d.created_at == test_time | ||||||
|  |     assert d.last_modified == test_time | ||||||
|  |     assert d.deleted_on == test_time | ||||||
|  | 
 | ||||||
|  |     assert d._get_audit_stamps().created.time == mce_builder.make_ts_millis(test_time) | ||||||
|  |     assert d._get_audit_stamps().lastModified.time == mce_builder.make_ts_millis( | ||||||
|  |         test_time | ||||||
|  |     ) | ||||||
|  |     # deleted should not be None after setting deleted_on | ||||||
|  |     deleted_stamp = d._get_audit_stamps().deleted | ||||||
|  |     assert deleted_stamp is not None | ||||||
|  |     assert deleted_stamp.time == mce_builder.make_ts_millis(test_time) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_dashboard_audit_stamps_integration() -> None: | ||||||
|  |     """Test audit stamps integration with other dashboard functionality.""" | ||||||
|  |     created_time = datetime(2023, 1, 1, 10, 0, 0) | ||||||
|  |     modified_time = datetime(2023, 1, 2, 14, 30, 0) | ||||||
|  | 
 | ||||||
|  |     d = Dashboard( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_integration_dashboard", | ||||||
|  |         display_name="Audit Integration Test", | ||||||
|  |         description="Testing audit stamps with other features", | ||||||
|  |         created_at=created_time, | ||||||
|  |         created_by="creator@example.com", | ||||||
|  |         last_modified=modified_time, | ||||||
|  |         last_modified_by="modifier@example.com", | ||||||
|  |         owners=[CorpUserUrn("owner@example.com")], | ||||||
|  |         tags=[TagUrn("test")], | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Verify audit stamps work alongside other properties | ||||||
|  |     assert d.created_at == created_time | ||||||
|  |     assert d.created_by == "urn:li:corpuser:creator@example.com" | ||||||
|  |     assert d.last_modified == modified_time | ||||||
|  |     assert d.last_modified_by == "urn:li:corpuser:modifier@example.com" | ||||||
|  | 
 | ||||||
|  |     # Verify other properties still work | ||||||
|  |     assert d.display_name == "Audit Integration Test" | ||||||
|  |     assert d.description == "Testing audit stamps with other features" | ||||||
|  |     assert d.owners is not None | ||||||
|  |     assert d.tags is not None | ||||||
|  | 
 | ||||||
|  |     # Test that modifying audit stamps doesn't affect other properties | ||||||
|  |     new_modified_time = datetime(2023, 1, 3, 16, 0, 0) | ||||||
|  |     d.set_last_modified(new_modified_time) | ||||||
|  |     d.set_last_modified_by("new_modifier@example.com") | ||||||
|  | 
 | ||||||
|  |     assert d.last_modified == new_modified_time | ||||||
|  |     assert d.last_modified_by == "urn:li:corpuser:new_modifier@example.com" | ||||||
|  | 
 | ||||||
|  |     # Other properties should remain unchanged | ||||||
|  |     assert d.display_name == "Audit Integration Test" | ||||||
|  |     assert d.description == "Testing audit stamps with other features" | ||||||
|  |     assert d.owners is not None | ||||||
|  |     assert d.tags is not None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_dashboard_audit_stamps_golden() -> None: | ||||||
|  |     """Test audit stamps with golden file comparison.""" | ||||||
|  |     created_time = datetime(2023, 1, 1, 10, 0, 0) | ||||||
|  |     modified_time = datetime(2023, 1, 2, 14, 30, 0) | ||||||
|  |     deleted_time = datetime(2023, 1, 3, 16, 45, 0) | ||||||
|  | 
 | ||||||
|  |     d = Dashboard( | ||||||
|  |         platform="looker", | ||||||
|  |         name="audit_golden_dashboard", | ||||||
|  |         display_name="Audit Golden Test", | ||||||
|  |         description="Testing audit stamps with golden files", | ||||||
|  |         created_at=created_time, | ||||||
|  |         created_by="creator@example.com", | ||||||
|  |         last_modified=modified_time, | ||||||
|  |         last_modified_by="modifier@example.com", | ||||||
|  |         deleted_on=deleted_time, | ||||||
|  |         deleted_by="deleter@example.com", | ||||||
|  |         owners=[CorpUserUrn("owner@example.com")], | ||||||
|  |         tags=[TagUrn("audit"), TagUrn("test")], | ||||||
|  |         custom_properties={"audit_test": "true"}, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Generate golden file for audit stamps functionality | ||||||
|  |     assert_entity_golden( | ||||||
|  |         d, | ||||||
|  |         _GOLDEN_DIR / "test_dashboard_audit_stamps_golden.json", | ||||||
|  |         ["lastRefreshed"],  # Exclude timestamp fields that might vary between test runs | ||||||
|  |     ) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Anush Kumar
						Anush Kumar