openlineage-python 1.22.0__tar.gz → 1.24.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.22.0 → openlineage_python-1.24.0}/PKG-INFO +1 -1
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/client.py +165 -14
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/constants.py +1 -1
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/facet_v2.py +2 -0
- openlineage_python-1.24.0/openlineage/client/facets.py +10 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/filter.py +14 -6
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/column_lineage_dataset.py +13 -1
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/datasource_dataset.py +2 -0
- openlineage_python-1.24.0/openlineage/client/generated/environment_variables_run.py +35 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/nominal_time_run.py +2 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/composite.py +7 -2
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/factory.py +6 -1
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/http.py +7 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/kafka.py +3 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/transport.py +4 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/utils.py +19 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/pyproject.toml +1 -1
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/redact_fields.yml +7 -0
- openlineage_python-1.24.0/tests/test_client.py +853 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_composite.py +10 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_facet_v2.py +5 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_http.py +119 -1
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_kafka.py +16 -0
- openlineage_python-1.24.0/tests/test_utils.py +65 -0
- openlineage_python-1.22.0/tests/test_client.py +0 -391
- openlineage_python-1.22.0/tests/test_utils.py +0 -30
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/.gitignore +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/README.md +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/Makefile +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/conf.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/convert.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/index.rst +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/make.bat +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/source/openlineage.client.rst +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/__init__.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/event_v2.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/facet.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/base.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/data_quality_assertions_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/data_quality_metrics_input_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/dataset_version_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/documentation_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/documentation_job.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/error_message_run.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/external_query_run.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/extraction_error_run.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/job_type_job.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/lifecycle_state_change_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/output_statistics_output_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/ownership_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/ownership_job.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/parent_run.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/processing_engine_run.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/schema_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/source_code_job.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/source_code_location_job.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/sql_job.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/storage_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/symlinks_dataset.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/py.typed +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/run.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/serde.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/__init__.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/console.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/file.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/msk_iam.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/noop.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/uuid.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/__init__.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/config.yml +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/exact_filter.yml +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/http.yml +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/openlineage.yml +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/regex_filter.yml +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/conftest.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/example_full_event.json +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/nominal_time_without_end.json +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/serde_example_dataset_event.json +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/serde_example_job_event.json +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/serde_example_run_event.json +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_events.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_facet.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_factory.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_file.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_msk_iam.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_uuid.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/transport.py +0 -0
- {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tox.ini +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import logging
|
|
6
7
|
import os
|
|
7
8
|
import warnings
|
|
@@ -9,16 +10,28 @@ from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
|
|
|
9
10
|
|
|
10
11
|
import attr
|
|
11
12
|
import yaml
|
|
12
|
-
from openlineage.client.filter import Filter, create_filter
|
|
13
|
+
from openlineage.client.filter import Filter, FilterConfig, create_filter
|
|
13
14
|
from openlineage.client.serde import Serde
|
|
15
|
+
from openlineage.client.utils import deep_merge_dicts
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
16
18
|
from requests import Session
|
|
17
19
|
from requests.adapters import HTTPAdapter
|
|
18
20
|
|
|
21
|
+
import contextlib
|
|
22
|
+
|
|
19
23
|
from openlineage.client import event_v2
|
|
24
|
+
from openlineage.client.facets import FacetsConfig
|
|
25
|
+
from openlineage.client.generated.environment_variables_run import (
|
|
26
|
+
EnvironmentVariable,
|
|
27
|
+
EnvironmentVariablesRunFacet,
|
|
28
|
+
)
|
|
20
29
|
from openlineage.client.run import DatasetEvent, JobEvent, RunEvent
|
|
21
|
-
from openlineage.client.transport import
|
|
30
|
+
from openlineage.client.transport import (
|
|
31
|
+
Transport,
|
|
32
|
+
TransportFactory,
|
|
33
|
+
get_default_factory,
|
|
34
|
+
)
|
|
22
35
|
from openlineage.client.transport.http import HttpConfig, HttpTransport, create_token_provider
|
|
23
36
|
from openlineage.client.transport.noop import NoopConfig, NoopTransport
|
|
24
37
|
|
|
@@ -35,11 +48,32 @@ class OpenLineageClientOptions:
|
|
|
35
48
|
adapter: HTTPAdapter = attr.ib(default=None)
|
|
36
49
|
|
|
37
50
|
|
|
51
|
+
@attr.s
|
|
52
|
+
class OpenLineageConfig:
|
|
53
|
+
transport: dict[str, Any] | None = attr.ib(factory=dict)
|
|
54
|
+
facets: FacetsConfig = attr.ib(factory=FacetsConfig)
|
|
55
|
+
filters: list[FilterConfig] = attr.ib(factory=list)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, params: dict[str, Any]) -> OpenLineageConfig:
|
|
59
|
+
config = cls()
|
|
60
|
+
if "transport" in params:
|
|
61
|
+
config.transport = params["transport"]
|
|
62
|
+
if "facets" in params:
|
|
63
|
+
config.facets = FacetsConfig(**params["facets"])
|
|
64
|
+
if "filters" in params:
|
|
65
|
+
config.filters = [FilterConfig(**filter_config) for filter_config in params["filters"]]
|
|
66
|
+
return config
|
|
67
|
+
|
|
68
|
+
|
|
38
69
|
log = logging.getLogger(__name__)
|
|
39
70
|
_T = TypeVar("_T", bound="OpenLineageClient")
|
|
40
71
|
|
|
41
72
|
|
|
42
73
|
class OpenLineageClient:
|
|
74
|
+
DYNAMIC_ENV_VARS_PREFIX = "OPENLINEAGE__"
|
|
75
|
+
DEFAULT_URL_TRANSPORT_NAME = "default_http"
|
|
76
|
+
|
|
43
77
|
def __init__( # noqa: PLR0913
|
|
44
78
|
self,
|
|
45
79
|
url: str | None = None,
|
|
@@ -47,6 +81,8 @@ class OpenLineageClient:
|
|
|
47
81
|
session: Session | None = None,
|
|
48
82
|
transport: Transport | None = None,
|
|
49
83
|
factory: TransportFactory | None = None,
|
|
84
|
+
*,
|
|
85
|
+
config: dict[str, str] | None = None,
|
|
50
86
|
) -> None:
|
|
51
87
|
# Set parent's logging level if environment variable is present
|
|
52
88
|
custom_logging_level = os.getenv("OPENLINEAGE_CLIENT_LOGGING", None)
|
|
@@ -62,7 +98,11 @@ class OpenLineageClient:
|
|
|
62
98
|
|
|
63
99
|
# Make config ellipsis - as a guard value to not try to
|
|
64
100
|
# reload yaml each time config is referred to.
|
|
65
|
-
self._config:
|
|
101
|
+
self._config: OpenLineageConfig | None = None
|
|
102
|
+
|
|
103
|
+
self.user_defined_config: dict[str, str] | None = config
|
|
104
|
+
|
|
105
|
+
self._alias_env_vars()
|
|
66
106
|
|
|
67
107
|
self.transport = self._resolve_transport(
|
|
68
108
|
url=url, options=options, session=session, transport=transport, factory=factory
|
|
@@ -70,7 +110,7 @@ class OpenLineageClient:
|
|
|
70
110
|
log.info("OpenLineageClient will use `%s` transport", self.transport.kind)
|
|
71
111
|
|
|
72
112
|
self._filters: list[Filter] = []
|
|
73
|
-
for conf in self.config.
|
|
113
|
+
for conf in self.config.filters:
|
|
74
114
|
_filter = create_filter(conf)
|
|
75
115
|
if _filter:
|
|
76
116
|
self._filters.append(_filter)
|
|
@@ -86,7 +126,7 @@ class OpenLineageClient:
|
|
|
86
126
|
|
|
87
127
|
@classmethod
|
|
88
128
|
def from_dict(cls: type[_T], config: dict[str, str]) -> _T:
|
|
89
|
-
return cls(
|
|
129
|
+
return cls(config=config)
|
|
90
130
|
|
|
91
131
|
def filter_event(
|
|
92
132
|
self,
|
|
@@ -114,6 +154,8 @@ class OpenLineageClient:
|
|
|
114
154
|
log.debug("OpenLineage event has been filtered out and will not be emitted.")
|
|
115
155
|
return
|
|
116
156
|
|
|
157
|
+
event = self.add_environment_facets(event)
|
|
158
|
+
|
|
117
159
|
if log.isEnabledFor(logging.DEBUG):
|
|
118
160
|
val = Serde.to_json(event).encode("utf-8")
|
|
119
161
|
log.debug("OpenLineageClient will emit event %s", val)
|
|
@@ -121,14 +163,36 @@ class OpenLineageClient:
|
|
|
121
163
|
log.debug("OpenLineage event successfully emitted.")
|
|
122
164
|
|
|
123
165
|
@property
|
|
124
|
-
def config(self) ->
|
|
125
|
-
"""
|
|
166
|
+
def config(self) -> OpenLineageConfig:
|
|
167
|
+
"""
|
|
168
|
+
Retrieves the OpenLineage configuration.
|
|
169
|
+
|
|
170
|
+
This property method returns the content of the OpenLineage YAML config file.
|
|
171
|
+
The configuration is determined by merging sources in the following order of precedence:
|
|
172
|
+
1. User-defined configuration passed to the client constructor.
|
|
173
|
+
2. YAML config file located in one of the following paths:
|
|
174
|
+
- Path specified by the `OPENLINEAGE_CONFIG` environment variable.
|
|
175
|
+
- Current working directory.
|
|
176
|
+
- `$HOME/.openlineage`.
|
|
177
|
+
3. Environment variables with the `OPENLINEAGE__` prefix.
|
|
178
|
+
If the configuration is not already loaded, it will be constructed by merging the above sources.
|
|
179
|
+
In case of a TypeError during the parsing of the configuration, a ValueError will be raised indicating
|
|
180
|
+
that the structure of the config does not match the expected format.
|
|
181
|
+
"""
|
|
126
182
|
if self._config is None:
|
|
127
|
-
|
|
128
|
-
if
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
self.
|
|
183
|
+
config_dict: dict[str, Any] = {}
|
|
184
|
+
if self.user_defined_config:
|
|
185
|
+
config_dict = self.user_defined_config
|
|
186
|
+
if config_path := self._find_yaml_config_path():
|
|
187
|
+
config_dict = deep_merge_dicts(self._get_config_file_content(config_path), config_dict)
|
|
188
|
+
if config_from_env_vars := self._load_config_from_env_variables():
|
|
189
|
+
config_dict = deep_merge_dicts(config_from_env_vars, config_dict)
|
|
190
|
+
try:
|
|
191
|
+
self._config = OpenLineageConfig.from_dict(config_dict)
|
|
192
|
+
except TypeError as e:
|
|
193
|
+
# raise exception that structure of the config does not match
|
|
194
|
+
msg = "Failed to parse OpenLineage config."
|
|
195
|
+
raise ValueError(msg) from e
|
|
132
196
|
return self._config
|
|
133
197
|
|
|
134
198
|
def _resolve_transport(self, **kwargs: Any) -> Transport:
|
|
@@ -156,9 +220,9 @@ class OpenLineageClient:
|
|
|
156
220
|
return cast(Transport, kwargs["transport"])
|
|
157
221
|
|
|
158
222
|
# 3. Check if transport configuration is provided in YAML config file
|
|
159
|
-
if self.config.get("
|
|
223
|
+
if self.config.transport and self.config.transport.get("type"):
|
|
160
224
|
factory = kwargs.get("factory") or get_default_factory()
|
|
161
|
-
return factory.create(self.config
|
|
225
|
+
return factory.create(self.config.transport)
|
|
162
226
|
|
|
163
227
|
# 4. Check legacy HTTP transport initialization with url and options
|
|
164
228
|
if kwargs.get("url"):
|
|
@@ -245,3 +309,90 @@ class OpenLineageClient:
|
|
|
245
309
|
session=session,
|
|
246
310
|
),
|
|
247
311
|
)
|
|
312
|
+
|
|
313
|
+
def _alias_env_vars(self) -> None:
|
|
314
|
+
default_transport_name = self.DEFAULT_URL_TRANSPORT_NAME.upper()
|
|
315
|
+
if url := os.environ.get("OPENLINEAGE_URL"):
|
|
316
|
+
if any(
|
|
317
|
+
k.startswith(f"OPENLINEAGE__TRANSPORT__TRANSPORTS__{default_transport_name}")
|
|
318
|
+
for k in os.environ
|
|
319
|
+
):
|
|
320
|
+
log.warning(
|
|
321
|
+
"%s already found in environment variables, skipping aliasing OPENLINEAGE_URL",
|
|
322
|
+
default_transport_name,
|
|
323
|
+
)
|
|
324
|
+
return
|
|
325
|
+
os.environ[f"OPENLINEAGE__TRANSPORT__TRANSPORTS__{default_transport_name}__TYPE"] = "http"
|
|
326
|
+
os.environ[f"OPENLINEAGE__TRANSPORT__TRANSPORTS__{default_transport_name}__URL"] = url
|
|
327
|
+
if api_key := os.environ.get("OPENLINEAGE_API_KEY"):
|
|
328
|
+
os.environ[
|
|
329
|
+
f"OPENLINEAGE__TRANSPORT__TRANSPORTS__{default_transport_name}__AUTH"
|
|
330
|
+
] = json.dumps(
|
|
331
|
+
{
|
|
332
|
+
"type": "api_key",
|
|
333
|
+
"apiKey": api_key,
|
|
334
|
+
}
|
|
335
|
+
)
|
|
336
|
+
if endpoint := os.environ.get("OPENLINEAGE_ENDPOINT"):
|
|
337
|
+
os.environ[
|
|
338
|
+
f"OPENLINEAGE__TRANSPORT__TRANSPORTS__{default_transport_name}__ENDPOINT"
|
|
339
|
+
] = endpoint
|
|
340
|
+
|
|
341
|
+
@classmethod
|
|
342
|
+
def _load_config_from_env_variables(cls) -> dict[str, Any] | None:
|
|
343
|
+
config: dict[str, Any] = {}
|
|
344
|
+
|
|
345
|
+
# get os.environ.items only starting with OPENLINEAGE_ prefix and reverse sort
|
|
346
|
+
# to make sure that top-level keys have precedence
|
|
347
|
+
env_vars = sorted(
|
|
348
|
+
filter(lambda k: k[0].startswith(cls.DYNAMIC_ENV_VARS_PREFIX), os.environ.items()), reverse=True
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
for env_key, env_value in env_vars:
|
|
352
|
+
keys = env_key[len(cls.DYNAMIC_ENV_VARS_PREFIX) :].split("__")
|
|
353
|
+
|
|
354
|
+
# Parse value (try to parse as JSON, otherwise lowercase the value)
|
|
355
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
356
|
+
env_value = json.loads(env_value) # noqa: PLW2901
|
|
357
|
+
|
|
358
|
+
cls._insert_into_config(config, keys, env_value)
|
|
359
|
+
|
|
360
|
+
return config
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _insert_into_config(config: dict[str, Any], key_path: list[str], value: str) -> None:
|
|
364
|
+
keys = [key.lower() for key in key_path]
|
|
365
|
+
|
|
366
|
+
current = config
|
|
367
|
+
for key in keys[:-1]:
|
|
368
|
+
if key not in current:
|
|
369
|
+
current[key] = {}
|
|
370
|
+
current = current[key]
|
|
371
|
+
|
|
372
|
+
# Overwrite if key already exists
|
|
373
|
+
current[keys[-1]] = value
|
|
374
|
+
|
|
375
|
+
def add_environment_facets(self, event: Event) -> Event:
|
|
376
|
+
"""
|
|
377
|
+
Adds environment variables as facets to the event object.
|
|
378
|
+
"""
|
|
379
|
+
if isinstance(event, RunEvent) and (env_vars := self._collect_environment_variables()):
|
|
380
|
+
event.run.facets["environmentVariables"] = EnvironmentVariablesRunFacet(
|
|
381
|
+
environmentVariables=[
|
|
382
|
+
EnvironmentVariable(name=name, value=value) for name, value in env_vars.items()
|
|
383
|
+
]
|
|
384
|
+
)
|
|
385
|
+
return event
|
|
386
|
+
|
|
387
|
+
def _collect_environment_variables(self) -> dict[str, str]:
|
|
388
|
+
"""
|
|
389
|
+
Collects and returns a dictionary of relevant environment variables.
|
|
390
|
+
"""
|
|
391
|
+
filtered_vars = {k: v for k, v in os.environ.items() if k in self.config.facets.environment_variables}
|
|
392
|
+
missing_vars = set(self.config.facets.environment_variables) - set(filtered_vars)
|
|
393
|
+
if missing_vars:
|
|
394
|
+
log.warning(
|
|
395
|
+
"The following environment variables are missing: %s when adding to OpenLineage event",
|
|
396
|
+
missing_vars,
|
|
397
|
+
)
|
|
398
|
+
return filtered_vars
|
|
@@ -9,6 +9,7 @@ from openlineage.client.generated import (
|
|
|
9
9
|
datasource_dataset,
|
|
10
10
|
documentation_dataset,
|
|
11
11
|
documentation_job,
|
|
12
|
+
environment_variables_run,
|
|
12
13
|
error_message_run,
|
|
13
14
|
external_query_run,
|
|
14
15
|
extraction_error_run,
|
|
@@ -54,6 +55,7 @@ __all__ = [
|
|
|
54
55
|
"datasource_dataset",
|
|
55
56
|
"documentation_dataset",
|
|
56
57
|
"documentation_job",
|
|
58
|
+
"environment_variables_run",
|
|
57
59
|
"error_message_run",
|
|
58
60
|
"external_query_run",
|
|
59
61
|
"extraction_error_run",
|
|
@@ -5,12 +5,20 @@ from __future__ import annotations
|
|
|
5
5
|
import re
|
|
6
6
|
import typing
|
|
7
7
|
|
|
8
|
+
import attr
|
|
8
9
|
from openlineage.client.event_v2 import RunEvent as RunEvent_v2
|
|
9
10
|
from openlineage.client.run import RunEvent
|
|
10
11
|
|
|
11
12
|
RunEventType = typing.Union[RunEvent, RunEvent_v2]
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
@attr.s
|
|
16
|
+
class FilterConfig:
|
|
17
|
+
type: str | None = attr.ib(default=None)
|
|
18
|
+
match: str | None = attr.ib(default=None)
|
|
19
|
+
regex: str | None = attr.ib(default=None)
|
|
20
|
+
|
|
21
|
+
|
|
14
22
|
class Filter:
|
|
15
23
|
def filter_event(self, event: RunEventType) -> RunEventType | None:
|
|
16
24
|
...
|
|
@@ -36,12 +44,12 @@ class RegexFilter(Filter):
|
|
|
36
44
|
return event
|
|
37
45
|
|
|
38
46
|
|
|
39
|
-
def create_filter(conf:
|
|
40
|
-
if
|
|
47
|
+
def create_filter(conf: FilterConfig) -> Filter | None:
|
|
48
|
+
if not conf.type:
|
|
41
49
|
return None
|
|
42
50
|
# Switch in 3.10 🙂
|
|
43
|
-
if conf
|
|
44
|
-
return ExactMatchFilter(match=conf
|
|
45
|
-
if conf
|
|
46
|
-
return RegexFilter(regex=conf
|
|
51
|
+
if conf.type == "exact" and conf.match:
|
|
52
|
+
return ExactMatchFilter(match=conf.match)
|
|
53
|
+
if conf.type == "regex" and conf.regex:
|
|
54
|
+
return RegexFilter(regex=conf.regex)
|
|
47
55
|
return None
|
|
@@ -15,9 +15,15 @@ class ColumnLineageDatasetFacet(DatasetFacet):
|
|
|
15
15
|
fields: dict[str, Fields]
|
|
16
16
|
"""Column level lineage that maps output fields into input fields used to evaluate them."""
|
|
17
17
|
|
|
18
|
+
dataset: list[InputField] | None = attr.field(factory=list)
|
|
19
|
+
"""
|
|
20
|
+
Column level lineage that affects the whole dataset. This includes filtering, sorting, grouping
|
|
21
|
+
(aggregates), joining, window functions, etc.
|
|
22
|
+
"""
|
|
23
|
+
|
|
18
24
|
@staticmethod
|
|
19
25
|
def _get_schema() -> str:
|
|
20
|
-
return "https://openlineage.io/spec/facets/1-
|
|
26
|
+
return "https://openlineage.io/spec/facets/1-2-0/ColumnLineageDatasetFacet.json#/$defs/ColumnLineageDatasetFacet"
|
|
21
27
|
|
|
22
28
|
|
|
23
29
|
@attr.define
|
|
@@ -35,6 +41,8 @@ class Fields(RedactMixin):
|
|
|
35
41
|
|
|
36
42
|
@attr.define
|
|
37
43
|
class InputField(RedactMixin):
|
|
44
|
+
"""Represents a single dependency on some field (column)."""
|
|
45
|
+
|
|
38
46
|
namespace: str
|
|
39
47
|
"""The input dataset namespace"""
|
|
40
48
|
|
|
@@ -47,6 +55,10 @@ class InputField(RedactMixin):
|
|
|
47
55
|
transformations: list[Transformation] | None = attr.field(factory=list)
|
|
48
56
|
_skip_redact: ClassVar[list[str]] = ["namespace", "name", "field"]
|
|
49
57
|
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _get_schema() -> str:
|
|
60
|
+
return "https://openlineage.io/spec/facets/1-2-0/ColumnLineageDatasetFacet.json#/$defs/InputField"
|
|
61
|
+
|
|
50
62
|
|
|
51
63
|
@attr.define
|
|
52
64
|
class Transformation(RedactMixin):
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2018-2024 contributors to the OpenLineage project
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
import attr
|
|
9
|
+
from openlineage.client.generated.base import RunFacet
|
|
10
|
+
from openlineage.client.utils import RedactMixin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@attr.define
|
|
14
|
+
class EnvironmentVariable(RedactMixin):
|
|
15
|
+
name: str
|
|
16
|
+
"""The name of the environment variable."""
|
|
17
|
+
|
|
18
|
+
value: str
|
|
19
|
+
"""The value of the environment variable."""
|
|
20
|
+
|
|
21
|
+
_skip_redact: ClassVar[list[str]] = ["name"]
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _get_schema() -> str:
|
|
25
|
+
return "https://openlineage.io/spec/facets/1-0-0/EnvironmentVariablesRunFacet.json#/$defs/EnvironmentVariable"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@attr.define
|
|
29
|
+
class EnvironmentVariablesRunFacet(RunFacet):
|
|
30
|
+
environmentVariables: list[EnvironmentVariable] # noqa: N815
|
|
31
|
+
"""The environment variables for the run."""
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _get_schema() -> str:
|
|
35
|
+
return "https://openlineage.io/spec/facets/1-0-0/EnvironmentVariablesRunFacet.json#/$defs/EnvironmentVariablesRunFacet"
|
{openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/composite.py
RENAMED
|
@@ -35,7 +35,7 @@ class CompositeConfig(Config):
|
|
|
35
35
|
transport will halt the emission process for subsequent transports.
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
transports: list[dict[str, Any]] = attr.ib()
|
|
38
|
+
transports: list[dict[str, Any]] | dict[str, dict[str, Any]] = attr.ib()
|
|
39
39
|
continue_on_failure: bool = attr.ib(default=True)
|
|
40
40
|
|
|
41
41
|
@classmethod
|
|
@@ -63,7 +63,12 @@ class CompositeTransport(Transport):
|
|
|
63
63
|
from openlineage.client.transport import get_default_factory
|
|
64
64
|
|
|
65
65
|
transports = []
|
|
66
|
-
|
|
66
|
+
config_transports = self.config.transports
|
|
67
|
+
if isinstance(config_transports, dict):
|
|
68
|
+
config_transports = [
|
|
69
|
+
{**config, "name": name} for name, config in config_transports.items() if config
|
|
70
|
+
]
|
|
71
|
+
for transport_config in config_transports:
|
|
67
72
|
transports.append(get_default_factory().create(transport_config))
|
|
68
73
|
return transports
|
|
69
74
|
|
{openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/factory.py
RENAMED
|
@@ -50,6 +50,8 @@ class DefaultTransportFactory(TransportFactory):
|
|
|
50
50
|
msg = "You need to pass transport type in config."
|
|
51
51
|
raise TypeError(msg) from None
|
|
52
52
|
|
|
53
|
+
transport_name = config.pop("name", None)
|
|
54
|
+
|
|
53
55
|
transport_class_type_or_str = self.transports.get(transport_type, transport_type)
|
|
54
56
|
|
|
55
57
|
if isinstance(transport_class_type_or_str, str):
|
|
@@ -68,4 +70,7 @@ class DefaultTransportFactory(TransportFactory):
|
|
|
68
70
|
msg = f"Config {config_class} has to be class, and subclass of Config"
|
|
69
71
|
raise TypeError(msg)
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
transport: Transport = transport_class(config_class.from_dict(config)) # type: ignore[call-arg]
|
|
74
|
+
if transport_name and not transport.name:
|
|
75
|
+
transport.name = transport_name
|
|
76
|
+
return transport
|
{openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/http.py
RENAMED
|
@@ -87,6 +87,8 @@ class HttpConfig(Config):
|
|
|
87
87
|
session: Session | None = attr.ib(default=None)
|
|
88
88
|
# not set by TransportFactory
|
|
89
89
|
adapter: HTTPAdapter | None = attr.ib(default=None)
|
|
90
|
+
# custom headers support
|
|
91
|
+
custom_headers: dict[str, str] = attr.ib(factory=dict)
|
|
90
92
|
|
|
91
93
|
@classmethod
|
|
92
94
|
def from_dict(cls, params: dict[str, Any]) -> HttpConfig:
|
|
@@ -150,6 +152,7 @@ class HttpTransport(Transport):
|
|
|
150
152
|
self.session.headers["Content-Type"] = "application/json"
|
|
151
153
|
auth_headers = self._auth_headers(config.auth)
|
|
152
154
|
self.session.headers.update(auth_headers)
|
|
155
|
+
self.session.headers.update(config.custom_headers)
|
|
153
156
|
self.timeout = config.timeout
|
|
154
157
|
self.verify = config.verify
|
|
155
158
|
self.compression = config.compression
|
|
@@ -168,6 +171,9 @@ class HttpTransport(Transport):
|
|
|
168
171
|
http_client.HTTPConnection.debuglevel = 0
|
|
169
172
|
body, headers = self._prepare_request(Serde.to_json(event))
|
|
170
173
|
|
|
174
|
+
# Update headers with custom headers from the config
|
|
175
|
+
headers.update(self.config.custom_headers)
|
|
176
|
+
|
|
171
177
|
if self.session:
|
|
172
178
|
resp = self.session.post(
|
|
173
179
|
url=urljoin(self.url, self.endpoint),
|
|
@@ -179,6 +185,7 @@ class HttpTransport(Transport):
|
|
|
179
185
|
else:
|
|
180
186
|
headers["Content-Type"] = "application/json"
|
|
181
187
|
headers.update(self._auth_headers(self.config.auth))
|
|
188
|
+
headers.update(self.config.custom_headers)
|
|
182
189
|
with Session() as session:
|
|
183
190
|
resp = session.post(
|
|
184
191
|
url=urljoin(self.url, self.endpoint),
|
{openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/kafka.py
RENAMED
|
@@ -41,6 +41,9 @@ class KafkaConfig(Config):
|
|
|
41
41
|
|
|
42
42
|
@classmethod
|
|
43
43
|
def from_dict(cls: type[_T], params: dict[str, Any]) -> _T:
|
|
44
|
+
# alias message_key to messageKey
|
|
45
|
+
if message_key := params.pop("message_key", None):
|
|
46
|
+
params["messageKey"] = params.get("messageKey") or message_key
|
|
44
47
|
if "config" not in params:
|
|
45
48
|
msg = "kafka `config` not passed to KafkaConfig"
|
|
46
49
|
raise RuntimeError(msg)
|
{openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/transport.py
RENAMED
|
@@ -20,6 +20,8 @@ from __future__ import annotations
|
|
|
20
20
|
|
|
21
21
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
22
22
|
|
|
23
|
+
import attr
|
|
24
|
+
|
|
23
25
|
if TYPE_CHECKING:
|
|
24
26
|
from openlineage.client.client import Event
|
|
25
27
|
|
|
@@ -27,6 +29,7 @@ if TYPE_CHECKING:
|
|
|
27
29
|
_T = TypeVar("_T", bound="Config")
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
@attr.s
|
|
30
33
|
class Config:
|
|
31
34
|
@classmethod
|
|
32
35
|
def from_dict(cls: type[_T], params: dict[str, Any]) -> _T: # noqa: ARG003
|
|
@@ -35,6 +38,7 @@ class Config:
|
|
|
35
38
|
|
|
36
39
|
class Transport:
|
|
37
40
|
kind: str | None = None
|
|
41
|
+
name: str | None = None
|
|
38
42
|
config_class: type[Config] = Config
|
|
39
43
|
|
|
40
44
|
def emit(self, event: Event) -> Any:
|
|
@@ -36,6 +36,25 @@ def get_only_specified_fields(clazz: type[Any], params: dict[str, Any]) -> dict[
|
|
|
36
36
|
return {key: value for key, value in params.items() if key in field_keys}
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def deep_merge_dicts(dict1: dict[Any, Any], dict2: dict[Any, Any]) -> dict[Any, Any]:
|
|
40
|
+
"""Deep merges two dictionaries.
|
|
41
|
+
|
|
42
|
+
This function merges two dictionaries while handling nested dictionaries.
|
|
43
|
+
For keys that exist in both dictionaries, the values from dict2 take precedence.
|
|
44
|
+
If a key exists in both dictionaries and the values are dictionaries themselves,
|
|
45
|
+
they are merged recursively.
|
|
46
|
+
This function merges only dictionaries. If key is of different type, e.g. list
|
|
47
|
+
it does not work properly.
|
|
48
|
+
"""
|
|
49
|
+
merged = dict1.copy()
|
|
50
|
+
for k, v in dict2.items():
|
|
51
|
+
if k in merged and isinstance(v, dict):
|
|
52
|
+
merged[k] = deep_merge_dicts(merged.get(k, {}), v)
|
|
53
|
+
else:
|
|
54
|
+
merged[k] = v
|
|
55
|
+
return merged
|
|
56
|
+
|
|
57
|
+
|
|
39
58
|
class RedactMixin:
|
|
40
59
|
_skip_redact: ClassVar[list[str]] = []
|
|
41
60
|
|
|
@@ -92,6 +92,13 @@
|
|
|
92
92
|
classes:
|
|
93
93
|
- class_name: DocumentationJobFacet
|
|
94
94
|
redact_fields: []
|
|
95
|
+
- module: environment_variables_run
|
|
96
|
+
classes:
|
|
97
|
+
- class_name: EnvironmentVariable
|
|
98
|
+
redact_fields:
|
|
99
|
+
- name
|
|
100
|
+
- class_name: EnvironmentVariablesRunFacet
|
|
101
|
+
redact_fields: []
|
|
95
102
|
- module: error_message_run
|
|
96
103
|
classes:
|
|
97
104
|
- class_name: ErrorMessageRunFacet
|