openlineage-python 1.32.0__tar.gz → 1.33.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/PKG-INFO +3 -1
  2. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/constants.py +1 -1
  3. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/facet_v2.py +2 -0
  4. openlineage_python-1.33.0/openlineage/client/generated/catalog_dataset.py +32 -0
  5. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/__init__.py +18 -5
  6. openlineage_python-1.33.0/openlineage/client/transport/amazon_datazone.py +77 -0
  7. openlineage_python-1.33.0/openlineage/client/transport/transform/__init__.py +19 -0
  8. openlineage_python-1.33.0/openlineage/client/transport/transform/transform.py +141 -0
  9. openlineage_python-1.33.0/openlineage/client/transport/transform/transformers/job_namespace_replace_transformer.py +74 -0
  10. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/pyproject.toml +4 -1
  11. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/redact_fields.yml +4 -0
  12. openlineage_python-1.33.0/tests/generator/__init__.py +2 -0
  13. openlineage_python-1.33.0/tests/test_amazon_datazone.py +123 -0
  14. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_uuid.py +5 -1
  15. openlineage_python-1.33.0/tests/transform/__init__.py +2 -0
  16. openlineage_python-1.33.0/tests/transform/test_transform.py +384 -0
  17. openlineage_python-1.33.0/tests/transform/transformers/__init__.py +2 -0
  18. openlineage_python-1.33.0/tests/transform/transformers/test_job_namespace_replace_transformer.py +184 -0
  19. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/.gitignore +0 -0
  20. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/README.md +0 -0
  21. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/Makefile +0 -0
  22. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/conf.py +0 -0
  23. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/convert.py +0 -0
  24. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/index.rst +0 -0
  25. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/make.bat +0 -0
  26. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/source/openlineage.client.rst +0 -0
  27. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/__init__.py +0 -0
  28. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/client.py +0 -0
  29. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/event_v2.py +0 -0
  30. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/facet.py +0 -0
  31. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/facets.py +0 -0
  32. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/filter.py +0 -0
  33. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/base.py +0 -0
  34. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/column_lineage_dataset.py +0 -0
  35. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/data_quality_assertions_dataset.py +0 -0
  36. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/data_quality_metrics_input_dataset.py +0 -0
  37. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/dataset_type_dataset.py +0 -0
  38. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/dataset_version_dataset.py +0 -0
  39. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/datasource_dataset.py +0 -0
  40. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/documentation_dataset.py +0 -0
  41. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/documentation_job.py +0 -0
  42. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/environment_variables_run.py +0 -0
  43. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/error_message_run.py +0 -0
  44. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/external_query_run.py +0 -0
  45. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/extraction_error_run.py +0 -0
  46. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/input_statistics_input_dataset.py +0 -0
  47. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/job_type_job.py +0 -0
  48. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/lifecycle_state_change_dataset.py +0 -0
  49. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/nominal_time_run.py +0 -0
  50. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/output_statistics_output_dataset.py +0 -0
  51. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/ownership_dataset.py +0 -0
  52. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/ownership_job.py +0 -0
  53. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/parent_run.py +0 -0
  54. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/processing_engine_run.py +0 -0
  55. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/schema_dataset.py +0 -0
  56. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/source_code_job.py +0 -0
  57. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/source_code_location_job.py +0 -0
  58. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/sql_job.py +0 -0
  59. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/storage_dataset.py +0 -0
  60. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/symlinks_dataset.py +0 -0
  61. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/tags_dataset.py +0 -0
  62. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/tags_job.py +0 -0
  63. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/tags_run.py +0 -0
  64. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/base.py +0 -0
  65. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/cli.py +0 -0
  66. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/generate.py +0 -0
  67. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/header.py +0 -0
  68. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/Enum.jinja2 +0 -0
  69. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/dataclass.jinja2 +0 -0
  70. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/facet_v2.jinja2 +0 -0
  71. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/root.jinja2 +0 -0
  72. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/validators.jinja2 +0 -0
  73. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/py.typed +0 -0
  74. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/run.py +0 -0
  75. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/serde.py +0 -0
  76. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/tags.py +0 -0
  77. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/composite.py +0 -0
  78. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/console.py +0 -0
  79. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/factory.py +0 -0
  80. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/file.py +0 -0
  81. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/http.py +0 -0
  82. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/kafka.py +0 -0
  83. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/msk_iam.py +0 -0
  84. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/noop.py +0 -0
  85. {openlineage_python-1.32.0/tests → openlineage_python-1.33.0/openlineage/client/transport/transform/transformers}/__init__.py +0 -0
  86. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/transport.py +0 -0
  87. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/utils.py +0 -0
  88. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/uuid.py +0 -0
  89. {openlineage_python-1.32.0/tests/generator → openlineage_python-1.33.0/tests}/__init__.py +0 -0
  90. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/config.yml +0 -0
  91. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/exact_filter.yml +0 -0
  92. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/http.yml +0 -0
  93. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/openlineage.yml +0 -0
  94. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/regex_filter.yml +0 -0
  95. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/conftest.py +0 -0
  96. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/example_full_event.json +0 -0
  97. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/generator/test_base.py +0 -0
  98. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/generator/test_cli.py +0 -0
  99. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/nominal_time_without_end.json +0 -0
  100. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/serde_example_dataset_event.json +0 -0
  101. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/serde_example_job_event.json +0 -0
  102. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/serde_example_run_event.json +0 -0
  103. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_client.py +0 -0
  104. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_composite.py +0 -0
  105. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_events.py +0 -0
  106. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_facet.py +0 -0
  107. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_facet_v2.py +0 -0
  108. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_factory.py +0 -0
  109. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_file.py +0 -0
  110. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_http.py +0 -0
  111. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_kafka.py +0 -0
  112. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_msk_iam.py +0 -0
  113. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_utils.py +0 -0
  114. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/transport.py +0 -0
  115. {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openlineage-python
3
- Version: 1.32.0
3
+ Version: 1.33.0
4
4
  Summary: OpenLineage Python Client
5
5
  Author-email: OpenLineage <info@openlineage.io>
6
6
  Keywords: openlineage
@@ -18,6 +18,8 @@ Requires-Dist: packaging>=21.0
18
18
  Requires-Dist: python-dateutil>=2.8.2
19
19
  Requires-Dist: pyyaml>=5.4
20
20
  Requires-Dist: requests>=2.32.3
21
+ Provides-Extra: datazone
22
+ Requires-Dist: boto3>=1.34.134; extra == 'datazone'
21
23
  Provides-Extra: docs
22
24
  Requires-Dist: beautifulsoup4>=4.12.0; extra == 'docs'
23
25
  Requires-Dist: pydata-sphinx-theme>=0.13; extra == 'docs'
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.32.0"
5
+ __version__ = "1.33.0"
6
6
 
7
7
  DEFAULT_TIMEOUT_MS = 5000
8
8
  DEFAULT_NAMESPACE_NAME = "default"
@@ -2,6 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
4
  from openlineage.client.generated import (
5
+ catalog_dataset,
5
6
  column_lineage_dataset,
6
7
  data_quality_assertions_dataset,
7
8
  data_quality_metrics_input_dataset,
@@ -53,6 +54,7 @@ __all__ = [
53
54
  "OutputDatasetFacet",
54
55
  "RunFacet",
55
56
  "set_producer",
57
+ "catalog_dataset",
56
58
  "column_lineage_dataset",
57
59
  "data_quality_assertions_dataset",
58
60
  "data_quality_metrics_input_dataset",
@@ -0,0 +1,32 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import attr
7
+ from openlineage.client.generated.base import DatasetFacet
8
+
9
+
10
+ @attr.define
11
+ class CatalogDatasetFacet(DatasetFacet):
12
+ framework: str
13
+ """The storage framework for which the catalog is configured"""
14
+
15
+ type: str
16
+ """Type of the catalog."""
17
+
18
+ name: str
19
+ """Name of the catalog, as configured in the source system."""
20
+
21
+ metadataUri: str | None = attr.field(default=None) # noqa: N815
22
+ """URI or connection string to the catalog, if applicable."""
23
+
24
+ warehouseUri: str | None = attr.field(default=None) # noqa: N815
25
+ """URI or connection string to the physical location of the data that the catalog describes."""
26
+
27
+ source: str | None = attr.field(default=None)
28
+ """Source system where the catalog is configured."""
29
+
30
+ @staticmethod
31
+ def _get_schema() -> str:
32
+ return "https://openlineage.io/spec/facets/1-0-0/CatalogDatasetFacet.json#/$defs/CatalogDatasetFacet"
@@ -2,6 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from __future__ import annotations
4
4
 
5
+ from openlineage.client.transport.amazon_datazone import AmazonDataZoneConfig, AmazonDataZoneTransport
5
6
  from openlineage.client.transport.composite import CompositeTransport
6
7
  from openlineage.client.transport.console import ConsoleTransport
7
8
  from openlineage.client.transport.factory import DefaultTransportFactory
@@ -10,6 +11,10 @@ from openlineage.client.transport.http import HttpConfig, HttpTransport
10
11
  from openlineage.client.transport.kafka import KafkaConfig, KafkaTransport
11
12
  from openlineage.client.transport.msk_iam import MSKIAMConfig, MSKIAMTransport
12
13
  from openlineage.client.transport.noop import NoopTransport
14
+ from openlineage.client.transport.transform.transform import (
15
+ TransformConfig,
16
+ TransformTransport,
17
+ )
13
18
  from openlineage.client.transport.transport import Config, Transport, TransportFactory
14
19
 
15
20
  _factory = DefaultTransportFactory()
@@ -20,6 +25,8 @@ _factory.register_transport(MSKIAMTransport.kind, MSKIAMTransport)
20
25
  _factory.register_transport(ConsoleTransport.kind, ConsoleTransport)
21
26
  _factory.register_transport(NoopTransport.kind, NoopTransport)
22
27
  _factory.register_transport(FileTransport.kind, FileTransport)
28
+ _factory.register_transport(TransformTransport.kind, TransformTransport)
29
+ _factory.register_transport(AmazonDataZoneTransport.kind, AmazonDataZoneTransport)
23
30
 
24
31
 
25
32
  def get_default_factory() -> DefaultTransportFactory:
@@ -34,17 +41,23 @@ def register_transport(clazz: type[Transport]) -> type[Transport]:
34
41
 
35
42
 
36
43
  __all__ = [
44
+ "register_transport",
45
+ "get_default_factory",
37
46
  "Config",
47
+ "Transport",
38
48
  "TransportFactory",
49
+ "AmazonDataZoneConfig",
50
+ "AmazonDataZoneTransport",
51
+ "CompositeTransport",
52
+ "ConsoleTransport",
53
+ "FileTransport",
39
54
  "HttpConfig",
40
55
  "HttpTransport",
41
56
  "KafkaConfig",
42
57
  "KafkaTransport",
43
- "MSKIAMTransport",
44
58
  "MSKIAMConfig",
45
- "ConsoleTransport",
59
+ "MSKIAMTransport",
46
60
  "NoopTransport",
47
- "Transport",
48
- "register_transport",
49
- "get_default_factory",
61
+ "TransformConfig",
62
+ "TransformTransport",
50
63
  ]
@@ -0,0 +1,77 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ from openlineage.client import event_v2
11
+ from openlineage.client.run import RunEvent
12
+ from openlineage.client.serde import Serde
13
+ from openlineage.client.transport.transport import Config, Transport
14
+
15
+ if TYPE_CHECKING:
16
+ from openlineage.client.client import Event
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class AmazonDataZoneConfig(Config):
23
+ domain_id: str
24
+ endpoint_override: str | None = None
25
+
26
+ @classmethod
27
+ def from_dict(cls, params: dict[str, Any]) -> AmazonDataZoneConfig:
28
+ if "domainId" not in params:
29
+ msg = "`domainId` key not passed to AmazonDataZoneConfig"
30
+ raise RuntimeError(msg)
31
+ return cls(domain_id=params["domainId"], endpoint_override=params.get("endpointOverride"))
32
+
33
+
34
+ class AmazonDataZoneTransport(Transport):
35
+ kind = "amazon_datazone_api"
36
+ config_class = AmazonDataZoneConfig
37
+
38
+ def __init__(self, config: AmazonDataZoneConfig) -> None:
39
+ self.config = config
40
+ self.datazone = None
41
+
42
+ self._setup_datazone(self.config.endpoint_override)
43
+ log.debug(
44
+ "Constructing OpenLineage transport that will send events to Amazon DataZone domain `%s`.",
45
+ config.domain_id,
46
+ )
47
+
48
+ def emit(self, event: Event) -> None:
49
+ if not isinstance(event, (RunEvent, event_v2.RunEvent)):
50
+ # DataZone only supports RunEvent
51
+ log.warning("DataZone only supports RunEvent")
52
+ return
53
+ try:
54
+ response = self.datazone.post_lineage_event( # type: ignore[attr-defined]
55
+ domainIdentifier=self.config.domain_id, event=Serde.to_json(event).encode("utf-8")
56
+ )
57
+ log.info(
58
+ "Successfully posted a LineageEvent: %s in Domain: %s", response["id"], response["domainId"]
59
+ )
60
+ except Exception as error: # noqa: BLE001
61
+ msg = f"Failed to send lineage event to DataZone Domain {self.config.domain_id}: {event}"
62
+ raise RuntimeError(msg) from error
63
+
64
+ def _setup_datazone(self, endpoint_url: str | None = None) -> None:
65
+ try:
66
+ import boto3 # type: ignore[import-untyped]
67
+
68
+ self.datazone = boto3.client("datazone", endpoint_url=endpoint_url)
69
+ except (ImportError, ModuleNotFoundError):
70
+ log.exception(
71
+ "OpenLineage client could not import the boto3 module. "
72
+ "This could be due to boto3 not being installed or being corrupted. "
73
+ "DataZoneTransport requires boto3 to function properly. "
74
+ "Please ensure boto3 is installed correctly. "
75
+ "You can install or reinstall it via `pip install boto3`"
76
+ )
77
+ raise
@@ -0,0 +1,19 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ from openlineage.client.transport.transform.transform import (
6
+ EventTransformer,
7
+ TransformConfig,
8
+ TransformTransport,
9
+ )
10
+ from openlineage.client.transport.transform.transformers.job_namespace_replace_transformer import (
11
+ JobNamespaceReplaceTransformer,
12
+ )
13
+
14
+ __all__ = [
15
+ "EventTransformer",
16
+ "TransformConfig",
17
+ "TransformTransport",
18
+ "JobNamespaceReplaceTransformer",
19
+ ]
@@ -0,0 +1,141 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import inspect
7
+ import logging
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ import attr
11
+ from openlineage.client.serde import Serde
12
+ from openlineage.client.transport.transport import Config, Transport
13
+ from openlineage.client.utils import get_only_specified_fields, import_from_string
14
+
15
+ if TYPE_CHECKING:
16
+ from openlineage.client.client import Event
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class EventTransformer:
22
+ """Base class for transformers of OpenLineage events."""
23
+
24
+ def __init__(self, properties: dict[str, Any]) -> None:
25
+ """Initialize the transformer with a dictionary of properties.
26
+
27
+ These properties can be used by subclasses to configure the transformation behavior.
28
+ """
29
+ self.properties = properties
30
+
31
+ def transform(self, event: Event) -> Event | None:
32
+ """Transform an OpenLineage event.
33
+
34
+ Subclasses must override this method to apply specific transformations
35
+ to the given event.
36
+
37
+ Args:
38
+ event: The event to be transformed.
39
+
40
+ Returns:
41
+ The transformed event, or None to not emit the event.
42
+ """
43
+ raise NotImplementedError
44
+
45
+ def __str__(self) -> str:
46
+ return f"<{self.__class__.__name__}({self.properties})>"
47
+
48
+
49
+ @attr.s
50
+ class TransformConfig(Config):
51
+ """Configuration class for transform transport.
52
+
53
+ Holds configuration details including inner transport settings, the name of the
54
+ transformer class to use, and optional transformer-specific properties.
55
+ """
56
+
57
+ transport: dict[str, Any] = attr.ib()
58
+ transformer_class: str = attr.ib()
59
+ transformer_properties: dict[str, Any] = attr.ib(default={})
60
+
61
+ @classmethod
62
+ def from_dict(cls, params: dict[str, Any]) -> TransformConfig:
63
+ """Create a TransformConfig instance from a dictionary of parameters.
64
+
65
+ Validates that required fields (`transport` and `transformer_class`) are present
66
+ and filters out unspecified fields based on the class definition.
67
+
68
+ Args:
69
+ params: Dictionary containing configuration parameters.
70
+
71
+ Returns:
72
+ An instance of TransformConfig populated with the provided data.
73
+
74
+ Raises:
75
+ RuntimeError: If required keys (`transport` or `transformer_class`) are missing.
76
+ """
77
+ if "transport" not in params:
78
+ msg = "'transport' not passed to TransformConfig"
79
+ raise RuntimeError(msg)
80
+ if "transformer_class" not in params:
81
+ msg = "'transformer_class' not passed to TransformConfig"
82
+ raise RuntimeError(msg)
83
+ return cls(**get_only_specified_fields(cls, params))
84
+
85
+
86
+ class TransformTransport(Transport):
87
+ """Transport that transforms events before emitting them.
88
+
89
+ This transport wraps another transport (defined in the configuration) and intercepts
90
+ events for transformation using a user-defined EventTransformer class. Events can be
91
+ modified or discarded prior to being sent.
92
+ """
93
+
94
+ kind = "transform"
95
+ config_class = TransformConfig
96
+
97
+ def __init__(self, config: TransformConfig) -> None:
98
+ """Initialize the TransformTransport with the given configuration.
99
+
100
+ Dynamically loads the transformer class specified in the config, validates it,
101
+ and initializes both the transformer and the inner transport.
102
+
103
+ Args:
104
+ config: Configuration object containing transport and transformer settings.
105
+
106
+ Raises:
107
+ TypeError: If the transformer is not a valid subclass of EventTransformer.
108
+ """
109
+ # Import within method to avoid circular import error
110
+ from openlineage.client.transport import get_default_factory
111
+
112
+ transformer_class = import_from_string(config.transformer_class)
113
+ if not inspect.isclass(transformer_class) or not issubclass(transformer_class, EventTransformer):
114
+ msg = f"Transformer `{transformer_class}` has to be class, and subclass of EventTransformer"
115
+ raise TypeError(msg)
116
+ self.transformer = transformer_class(properties=config.transformer_properties)
117
+ self.transport = get_default_factory().create(config.transport)
118
+
119
+ def emit(self, event: Event) -> Any:
120
+ """Transform and emit an OpenLineage event.
121
+
122
+ Applies the configured EventTransformer to the event. If the result is not `None`,
123
+ the transformed event is passed to the wrapped transport's `emit` method.
124
+
125
+ Args:
126
+ event: The OpenLineage event to be transformed and emitted.
127
+
128
+ Returns:
129
+ The result of the inner transport's emit call, or None if the event is dropped.
130
+ """
131
+ log.debug("Event before transformation: %s", Serde.to_json(event))
132
+ event = copy.deepcopy(event) # Copy to make sure the transformer does not modify original event
133
+
134
+ log.debug("Transforming OpenLineage event with %s", self.transformer)
135
+ transformed = self.transformer.transform(event)
136
+ if transformed is None:
137
+ log.info("Transformed OpenLineage event is None. Skipping emission.")
138
+ return None
139
+
140
+ log.debug("Event after transformation: %s", Serde.to_json(transformed))
141
+ return self.transport.emit(transformed)
@@ -0,0 +1,74 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from openlineage.client import event_v2
10
+ from openlineage.client.transport.transform.transform import EventTransformer
11
+
12
+ if TYPE_CHECKING:
13
+ from openlineage.client.client import Event
14
+ from openlineage.client.facet_v2 import parent_run
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ class JobNamespaceReplaceTransformer(EventTransformer):
20
+ """EventTransformer that replaces the Job namespace in OpenLineage events."""
21
+
22
+ def __init__(self, properties: dict[str, Any]) -> None:
23
+ if not properties.get("new_job_namespace"):
24
+ msg = f"`new_job_namespace` not passed to {self.__class__.__name__}"
25
+ raise RuntimeError(msg)
26
+ self.new_job_namespace = properties["new_job_namespace"]
27
+ self.include_parent_facet = str(properties.get("include_parent_facet", "true")).lower() == "true"
28
+ super().__init__(properties=properties)
29
+
30
+ def transform(self, event: Event) -> Event | None:
31
+ if not isinstance(event, (event_v2.RunEvent, event_v2.JobEvent)): # These events have Job
32
+ log.warning(
33
+ "Unsupported type: expected `RunEvent` or `JobEvent`, got `%s`. Skipping transformation.",
34
+ type(event),
35
+ )
36
+ return event
37
+
38
+ log.debug(
39
+ "Replacing OpenLineage job namespace: `%s` with: `%s` for single event.",
40
+ event.job.namespace,
41
+ self.new_job_namespace,
42
+ )
43
+ event.job.namespace = self.new_job_namespace
44
+
45
+ if not self.include_parent_facet:
46
+ log.debug("Set `include_parent_facet` to True to include replacement in ParentRunFacet.")
47
+ return event
48
+
49
+ if not isinstance(event, event_v2.RunEvent):
50
+ log.debug("Only `RunEvent` can have ParentRunFacet, skipping replacement in ParentRunFacet.")
51
+ return event
52
+
53
+ if not event.run.facets or not event.run.facets.get("parent"):
54
+ log.debug("Parent run facet not found, skipping replacement in ParentRunFacet.")
55
+ return event
56
+
57
+ # Mypy sees RunFacet, but we assume ParentRunFacet, so we ignore the check.
58
+ parent_facet: parent_run.ParentRunFacet = event.run.facets["parent"] # type: ignore[assignment]
59
+ log.debug(
60
+ "Replacing parent OpenLineage job namespace: `%s` with: `%s` for single event.",
61
+ parent_facet.job.namespace,
62
+ self.new_job_namespace,
63
+ )
64
+ parent_facet.job.namespace = self.new_job_namespace
65
+
66
+ if parent_facet.root:
67
+ log.debug(
68
+ "Replacing root OpenLineage job namespace: `%s` with: `%s` for single event.",
69
+ parent_facet.root.job.namespace,
70
+ self.new_job_namespace,
71
+ )
72
+ parent_facet.root.job.namespace = self.new_job_namespace
73
+
74
+ return event
@@ -6,7 +6,7 @@ requires = [
6
6
 
7
7
  [project]
8
8
  name = "openlineage-python"
9
- version = "1.32.0"
9
+ version = "1.33.0"
10
10
  description = "OpenLineage Python Client"
11
11
  readme = "README.md"
12
12
  keywords = [
@@ -38,6 +38,9 @@ optional-dependencies.msk-iam = [
38
38
  "aws-msk-iam-sasl-signer-python>=1.0.1",
39
39
  "confluent-kafka>=2.1.1",
40
40
  ]
41
+ optional-dependencies.datazone = [
42
+ "boto3>=1.34.134"
43
+ ]
41
44
  optional-dependencies.test = [
42
45
  "covdefaults>=2.3",
43
46
  "pytest>=7.3.1",
@@ -239,3 +239,7 @@
239
239
  redact_fields: []
240
240
  - class_name: TagsRunFacet
241
241
  redact_fields: []
242
+ - module: catalog_dataset
243
+ classes:
244
+ - class_name: CatalogDatasetFacet
245
+ redact_fields: []
@@ -0,0 +1,2 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,123 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ import pytest
9
+ from openlineage.client import OpenLineageClient
10
+ from openlineage.client.run import (
11
+ Dataset,
12
+ DatasetEvent,
13
+ Job,
14
+ JobEvent,
15
+ Run,
16
+ RunEvent,
17
+ RunState,
18
+ )
19
+ from openlineage.client.serde import Serde
20
+ from openlineage.client.transport.amazon_datazone import AmazonDataZoneConfig, AmazonDataZoneTransport
21
+
22
+ if TYPE_CHECKING:
23
+ from pytest_mock import MockerFixture
24
+
25
+
26
+ @pytest.fixture()
27
+ def domain_id() -> str:
28
+ return "dzd_a1b2c3d4e5f6g7"
29
+
30
+
31
+ @pytest.fixture()
32
+ def run_event() -> RunEvent:
33
+ return RunEvent(
34
+ eventType=RunState.START,
35
+ eventTime="2024-08-20T11:08:01.123456",
36
+ run=Run(runId="f8b61441-a9fb-4f65-a156-be466eb29832"),
37
+ job=Job(namespace="test-namespace", name="test-job"),
38
+ producer="prod",
39
+ schemaURL="schema",
40
+ )
41
+
42
+
43
+ @pytest.fixture()
44
+ def dataset_event() -> DatasetEvent:
45
+ return DatasetEvent(
46
+ eventTime="2024-08-20T11:08:01.123456",
47
+ dataset=Dataset(namespace="test-namespace", name="test-dataset"),
48
+ producer="prod",
49
+ schemaURL="schema",
50
+ )
51
+
52
+
53
+ @pytest.fixture()
54
+ def job_event() -> JobEvent:
55
+ return JobEvent(
56
+ eventTime="2024-08-20T11:08:01.123456",
57
+ job=Job(namespace="test-namespace", name="test-job"),
58
+ producer="prod",
59
+ schemaURL="schema",
60
+ )
61
+
62
+
63
+ def test_datazone_config_raises_when_missing_domain_id() -> None:
64
+ with pytest.raises(TypeError):
65
+ AmazonDataZoneConfig()
66
+
67
+
68
+ def test_client_with_amazon_datazone_transport_emits_run_event(
69
+ run_event: RunEvent, domain_id: str, mocker: MockerFixture
70
+ ) -> None:
71
+ mocker.patch("boto3.client")
72
+ config = AmazonDataZoneConfig(domain_id=domain_id)
73
+ transport = AmazonDataZoneTransport(config)
74
+ client = OpenLineageClient(transport=transport)
75
+
76
+ client.emit(run_event)
77
+
78
+ transport.datazone.post_lineage_event.assert_called_once_with(
79
+ domainIdentifier=domain_id,
80
+ event=Serde.to_json(run_event).encode("utf-8"),
81
+ )
82
+
83
+
84
+ def test_client_with_amazon_datazone_transport_skips_job_event(
85
+ job_event: JobEvent, mocker: MockerFixture
86
+ ) -> None:
87
+ mocker.patch("boto3.client")
88
+ config = AmazonDataZoneConfig(domain_id=domain_id)
89
+ transport = AmazonDataZoneTransport(config)
90
+ client = OpenLineageClient(transport=transport)
91
+
92
+ client.emit(job_event)
93
+
94
+ transport.datazone.post_lineage_event.assert_not_called()
95
+
96
+
97
+ def test_client_with_amazon_datazone_transport_skips_dataset_event(
98
+ dataset_event: DatasetEvent, mocker: MockerFixture
99
+ ) -> None:
100
+ mocker.patch("boto3.client")
101
+ config = AmazonDataZoneConfig(domain_id=domain_id)
102
+ transport = AmazonDataZoneTransport(config)
103
+ client = OpenLineageClient(transport=transport)
104
+
105
+ client.emit(dataset_event)
106
+
107
+ transport.datazone.post_lineage_event.assert_not_called()
108
+
109
+
110
+ def test_setup_datazone_raises_when_boto3_missing(mocker: MockerFixture) -> None:
111
+ mocker.patch("boto3.client", side_effect=ModuleNotFoundError("No module named 'boto3'"))
112
+ config = AmazonDataZoneConfig(domain_id=domain_id)
113
+
114
+ with pytest.raises(ModuleNotFoundError):
115
+ AmazonDataZoneTransport(config)
116
+
117
+
118
+ def test_setup_datazone_raises_when_boto3_corrupted(mocker: MockerFixture) -> None:
119
+ mocker.patch("boto3.client", side_effect=ImportError("Cannot import 'boto3'"))
120
+ config = AmazonDataZoneConfig(domain_id=domain_id)
121
+
122
+ with pytest.raises(ImportError):
123
+ AmazonDataZoneTransport(config)
@@ -12,7 +12,11 @@ def test_generate_new_uuid_returns_uuidv7():
12
12
  assert generate_new_uuid().version == 7 # noqa: PLR2004
13
13
 
14
14
 
15
- def test_generate_static_uuid_returns_different_result_on_each_call():
15
+ def test_generate_new_uuid_returns_different_result_on_each_call():
16
+ result1 = generate_new_uuid()
17
+ result2 = generate_new_uuid()
18
+ assert result1 != result2
19
+
16
20
  instant = datetime.now()
17
21
  result1 = generate_new_uuid(instant)
18
22
  result2 = generate_new_uuid(instant)
@@ -0,0 +1,2 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0