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.
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/PKG-INFO +3 -1
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/constants.py +1 -1
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/facet_v2.py +2 -0
- openlineage_python-1.33.0/openlineage/client/generated/catalog_dataset.py +32 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/__init__.py +18 -5
- openlineage_python-1.33.0/openlineage/client/transport/amazon_datazone.py +77 -0
- openlineage_python-1.33.0/openlineage/client/transport/transform/__init__.py +19 -0
- openlineage_python-1.33.0/openlineage/client/transport/transform/transform.py +141 -0
- openlineage_python-1.33.0/openlineage/client/transport/transform/transformers/job_namespace_replace_transformer.py +74 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/pyproject.toml +4 -1
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/redact_fields.yml +4 -0
- openlineage_python-1.33.0/tests/generator/__init__.py +2 -0
- openlineage_python-1.33.0/tests/test_amazon_datazone.py +123 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_uuid.py +5 -1
- openlineage_python-1.33.0/tests/transform/__init__.py +2 -0
- openlineage_python-1.33.0/tests/transform/test_transform.py +384 -0
- openlineage_python-1.33.0/tests/transform/transformers/__init__.py +2 -0
- openlineage_python-1.33.0/tests/transform/transformers/test_job_namespace_replace_transformer.py +184 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/.gitignore +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/README.md +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/Makefile +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/conf.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/convert.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/index.rst +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/make.bat +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/docs/source/openlineage.client.rst +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/__init__.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/client.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/event_v2.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/facet.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/facets.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/filter.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/base.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/column_lineage_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/data_quality_assertions_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/data_quality_metrics_input_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/dataset_type_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/dataset_version_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/datasource_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/documentation_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/documentation_job.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/environment_variables_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/error_message_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/external_query_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/extraction_error_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/input_statistics_input_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/job_type_job.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/lifecycle_state_change_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/nominal_time_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/output_statistics_output_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/ownership_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/ownership_job.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/parent_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/processing_engine_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/schema_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/source_code_job.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/source_code_location_job.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/sql_job.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/storage_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/symlinks_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/tags_dataset.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/tags_job.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generated/tags_run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/base.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/cli.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/generate.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/header.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/Enum.jinja2 +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/dataclass.jinja2 +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/facet_v2.jinja2 +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/root.jinja2 +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/generator/templates/validators.jinja2 +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/py.typed +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/run.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/serde.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/tags.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/composite.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/console.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/factory.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/file.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/http.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/kafka.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/msk_iam.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/noop.py +0 -0
- {openlineage_python-1.32.0/tests → openlineage_python-1.33.0/openlineage/client/transport/transform/transformers}/__init__.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/transport.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/utils.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/uuid.py +0 -0
- {openlineage_python-1.32.0/tests/generator → openlineage_python-1.33.0/tests}/__init__.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/config.yml +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/exact_filter.yml +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/http.yml +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/openlineage.yml +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/config/regex_filter.yml +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/conftest.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/example_full_event.json +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/generator/test_base.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/generator/test_cli.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/nominal_time_without_end.json +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/serde_example_dataset_event.json +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/serde_example_job_event.json +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/serde_example_run_event.json +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_client.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_composite.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_events.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_facet.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_facet_v2.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_factory.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_file.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_http.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_kafka.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_msk_iam.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/test_utils.py +0 -0
- {openlineage_python-1.32.0 → openlineage_python-1.33.0}/tests/transport.py +0 -0
- {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.
|
|
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,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"
|
{openlineage_python-1.32.0 → openlineage_python-1.33.0}/openlineage/client/transport/__init__.py
RENAMED
|
@@ -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
|
-
"
|
|
59
|
+
"MSKIAMTransport",
|
|
46
60
|
"NoopTransport",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
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.
|
|
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",
|
|
@@ -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
|
|
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)
|