openlineage-python 1.23.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.
Files changed (87) hide show
  1. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/PKG-INFO +1 -1
  2. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/client.py +98 -25
  3. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/constants.py +1 -1
  4. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/facet_v2.py +2 -0
  5. openlineage_python-1.24.0/openlineage/client/facets.py +10 -0
  6. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/filter.py +14 -6
  7. openlineage_python-1.24.0/openlineage/client/generated/environment_variables_run.py +35 -0
  8. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/transport.py +3 -0
  9. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/utils.py +19 -0
  10. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/pyproject.toml +1 -1
  11. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/redact_fields.yml +7 -0
  12. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_client.py +276 -7
  13. openlineage_python-1.24.0/tests/test_utils.py +65 -0
  14. openlineage_python-1.23.0/tests/test_utils.py +0 -30
  15. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/.gitignore +0 -0
  16. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/README.md +0 -0
  17. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/docs/Makefile +0 -0
  18. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/docs/conf.py +0 -0
  19. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/docs/convert.py +0 -0
  20. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/docs/index.rst +0 -0
  21. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/docs/make.bat +0 -0
  22. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/docs/source/openlineage.client.rst +0 -0
  23. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/__init__.py +0 -0
  24. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/event_v2.py +0 -0
  25. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/facet.py +0 -0
  26. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/base.py +0 -0
  27. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/column_lineage_dataset.py +0 -0
  28. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/data_quality_assertions_dataset.py +0 -0
  29. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/data_quality_metrics_input_dataset.py +0 -0
  30. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/dataset_version_dataset.py +0 -0
  31. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/datasource_dataset.py +0 -0
  32. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/documentation_dataset.py +0 -0
  33. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/documentation_job.py +0 -0
  34. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/error_message_run.py +0 -0
  35. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/external_query_run.py +0 -0
  36. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/extraction_error_run.py +0 -0
  37. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/job_type_job.py +0 -0
  38. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/lifecycle_state_change_dataset.py +0 -0
  39. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/nominal_time_run.py +0 -0
  40. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/output_statistics_output_dataset.py +0 -0
  41. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/ownership_dataset.py +0 -0
  42. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/ownership_job.py +0 -0
  43. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/parent_run.py +0 -0
  44. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/processing_engine_run.py +0 -0
  45. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/schema_dataset.py +0 -0
  46. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/source_code_job.py +0 -0
  47. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/source_code_location_job.py +0 -0
  48. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/sql_job.py +0 -0
  49. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/storage_dataset.py +0 -0
  50. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/generated/symlinks_dataset.py +0 -0
  51. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/py.typed +0 -0
  52. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/run.py +0 -0
  53. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/serde.py +0 -0
  54. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/__init__.py +0 -0
  55. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/composite.py +0 -0
  56. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/console.py +0 -0
  57. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/factory.py +0 -0
  58. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/file.py +0 -0
  59. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/http.py +0 -0
  60. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/kafka.py +0 -0
  61. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/msk_iam.py +0 -0
  62. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/transport/noop.py +0 -0
  63. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/openlineage/client/uuid.py +0 -0
  64. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/__init__.py +0 -0
  65. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/config/config.yml +0 -0
  66. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/config/exact_filter.yml +0 -0
  67. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/config/http.yml +0 -0
  68. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/config/openlineage.yml +0 -0
  69. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/config/regex_filter.yml +0 -0
  70. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/conftest.py +0 -0
  71. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/example_full_event.json +0 -0
  72. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/nominal_time_without_end.json +0 -0
  73. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/serde_example_dataset_event.json +0 -0
  74. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/serde_example_job_event.json +0 -0
  75. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/serde_example_run_event.json +0 -0
  76. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_composite.py +0 -0
  77. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_events.py +0 -0
  78. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_facet.py +0 -0
  79. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_facet_v2.py +0 -0
  80. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_factory.py +0 -0
  81. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_file.py +0 -0
  82. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_http.py +0 -0
  83. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_kafka.py +0 -0
  84. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_msk_iam.py +0 -0
  85. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/test_uuid.py +0 -0
  86. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tests/transport.py +0 -0
  87. {openlineage_python-1.23.0 → openlineage_python-1.24.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openlineage-python
3
- Version: 1.23.0
3
+ Version: 1.24.0
4
4
  Summary: OpenLineage Python Client
5
5
  Author-email: OpenLineage <info@openlineage.io>
6
6
  Keywords: openlineage
@@ -10,8 +10,9 @@ from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
10
10
 
11
11
  import attr
12
12
  import yaml
13
- from openlineage.client.filter import Filter, create_filter
13
+ from openlineage.client.filter import Filter, FilterConfig, create_filter
14
14
  from openlineage.client.serde import Serde
15
+ from openlineage.client.utils import deep_merge_dicts
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from requests import Session
@@ -20,8 +21,17 @@ if TYPE_CHECKING:
20
21
  import contextlib
21
22
 
22
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
+ )
23
29
  from openlineage.client.run import DatasetEvent, JobEvent, RunEvent
24
- from openlineage.client.transport import Transport, TransportFactory, get_default_factory
30
+ from openlineage.client.transport import (
31
+ Transport,
32
+ TransportFactory,
33
+ get_default_factory,
34
+ )
25
35
  from openlineage.client.transport.http import HttpConfig, HttpTransport, create_token_provider
26
36
  from openlineage.client.transport.noop import NoopConfig, NoopTransport
27
37
 
@@ -38,6 +48,24 @@ class OpenLineageClientOptions:
38
48
  adapter: HTTPAdapter = attr.ib(default=None)
39
49
 
40
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
+
41
69
  log = logging.getLogger(__name__)
42
70
  _T = TypeVar("_T", bound="OpenLineageClient")
43
71
 
@@ -53,6 +81,8 @@ class OpenLineageClient:
53
81
  session: Session | None = None,
54
82
  transport: Transport | None = None,
55
83
  factory: TransportFactory | None = None,
84
+ *,
85
+ config: dict[str, str] | None = None,
56
86
  ) -> None:
57
87
  # Set parent's logging level if environment variable is present
58
88
  custom_logging_level = os.getenv("OPENLINEAGE_CLIENT_LOGGING", None)
@@ -68,7 +98,9 @@ class OpenLineageClient:
68
98
 
69
99
  # Make config ellipsis - as a guard value to not try to
70
100
  # reload yaml each time config is referred to.
71
- self._config: dict[str, dict[str, str]] | None = None
101
+ self._config: OpenLineageConfig | None = None
102
+
103
+ self.user_defined_config: dict[str, str] | None = config
72
104
 
73
105
  self._alias_env_vars()
74
106
 
@@ -78,7 +110,7 @@ class OpenLineageClient:
78
110
  log.info("OpenLineageClient will use `%s` transport", self.transport.kind)
79
111
 
80
112
  self._filters: list[Filter] = []
81
- for conf in self.config.get("filters", []):
113
+ for conf in self.config.filters:
82
114
  _filter = create_filter(conf)
83
115
  if _filter:
84
116
  self._filters.append(_filter)
@@ -94,7 +126,7 @@ class OpenLineageClient:
94
126
 
95
127
  @classmethod
96
128
  def from_dict(cls: type[_T], config: dict[str, str]) -> _T:
97
- return cls(transport=get_default_factory().create(config=config))
129
+ return cls(config=config)
98
130
 
99
131
  def filter_event(
100
132
  self,
@@ -122,6 +154,8 @@ class OpenLineageClient:
122
154
  log.debug("OpenLineage event has been filtered out and will not be emitted.")
123
155
  return
124
156
 
157
+ event = self.add_environment_facets(event)
158
+
125
159
  if log.isEnabledFor(logging.DEBUG):
126
160
  val = Serde.to_json(event).encode("utf-8")
127
161
  log.debug("OpenLineageClient will emit event %s", val)
@@ -129,17 +163,39 @@ class OpenLineageClient:
129
163
  log.debug("OpenLineage event successfully emitted.")
130
164
 
131
165
  @property
132
- def config(self) -> dict[str, Any]:
133
- """Content of OpenLineage YAML config file."""
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
+ """
134
182
  if self._config is None:
135
- config_path = self._find_yaml_config_path()
136
- if config_path:
137
- self._config = self._get_config_file_content(config_path)
138
- else:
139
- self._config = {}
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
140
196
  return self._config
141
197
 
142
- def _resolve_transport(self, **kwargs: Any) -> Transport: # noqa: PLR0911
198
+ def _resolve_transport(self, **kwargs: Any) -> Transport:
143
199
  """
144
200
  Resolves the transport mechanism based on the provided arguments or environment settings.
145
201
 
@@ -164,9 +220,9 @@ class OpenLineageClient:
164
220
  return cast(Transport, kwargs["transport"])
165
221
 
166
222
  # 3. Check if transport configuration is provided in YAML config file
167
- if self.config.get("transport"):
223
+ if self.config.transport and self.config.transport.get("type"):
168
224
  factory = kwargs.get("factory") or get_default_factory()
169
- return factory.create(self.config["transport"])
225
+ return factory.create(self.config.transport)
170
226
 
171
227
  # 4. Check legacy HTTP transport initialization with url and options
172
228
  if kwargs.get("url"):
@@ -174,16 +230,11 @@ class OpenLineageClient:
174
230
  url=kwargs["url"], options=kwargs.get("options"), session=kwargs.get("session")
175
231
  )
176
232
 
177
- # 5. Check transport initialization with env variables
178
- if config := self._load_config_from_env_variables():
179
- factory = kwargs.get("factory") or get_default_factory()
180
- return factory.create(config["transport"])
181
-
182
- # 6. Check HTTP transport initialization with env variables
233
+ # 5. Check HTTP transport initialization with env variables
183
234
  if os.environ.get("OPENLINEAGE_URL"):
184
235
  return self._http_transport_from_env_variables()
185
236
 
186
- # 7. If all else fails, print events to console
237
+ # 6. If all else fails, print events to console
187
238
  from openlineage.client.transport.console import ConsoleConfig, ConsoleTransport
188
239
 
189
240
  log.warning("Couldn't find any OpenLineage transport configuration; will print events to console.")
@@ -306,9 +357,6 @@ class OpenLineageClient:
306
357
 
307
358
  cls._insert_into_config(config, keys, env_value)
308
359
 
309
- if (transport_config := config.get("transport")) is None or transport_config.get("type") is None:
310
- return None
311
-
312
360
  return config
313
361
 
314
362
  @staticmethod
@@ -323,3 +371,28 @@ class OpenLineageClient:
323
371
 
324
372
  # Overwrite if key already exists
325
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
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.23.0"
5
+ __version__ = "1.24.0"
6
6
 
7
7
  DEFAULT_TIMEOUT_MS = 5000
8
8
  DEFAULT_NAMESPACE_NAME = "default"
@@ -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",
@@ -0,0 +1,10 @@
1
+ # Copyright 2018-2024 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ import attr
6
+
7
+
8
+ @attr.s
9
+ class FacetsConfig:
10
+ environment_variables: list[str] = attr.ib(factory=list)
@@ -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: dict[str, str]) -> Filter | None:
40
- if "type" not in conf:
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["type"] == "exact":
44
- return ExactMatchFilter(match=conf["match"])
45
- if conf["type"] == "regex":
46
- return RegexFilter(regex=conf["regex"])
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
@@ -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"
@@ -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
@@ -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
 
@@ -6,7 +6,7 @@ requires = [
6
6
 
7
7
  [project]
8
8
  name = "openlineage-python"
9
- version = "1.23.0"
9
+ version = "1.24.0"
10
10
  description = "OpenLineage Python Client"
11
11
  readme = "README.md"
12
12
  keywords = [
@@ -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
@@ -9,7 +9,12 @@ from typing import TYPE_CHECKING
9
9
  from unittest.mock import MagicMock, patch
10
10
 
11
11
  import pytest
12
- from openlineage.client.client import OpenLineageClient, OpenLineageClientOptions
12
+ from openlineage.client.client import OpenLineageClient, OpenLineageClientOptions, OpenLineageConfig
13
+ from openlineage.client.facets import FacetsConfig
14
+ from openlineage.client.generated.environment_variables_run import (
15
+ EnvironmentVariable,
16
+ EnvironmentVariablesRunFacet,
17
+ )
13
18
  from openlineage.client.run import (
14
19
  SCHEMA_URL,
15
20
  Dataset,
@@ -351,22 +356,137 @@ def test_find_yaml_config_path_checks_all_paths(mock_is_file, mocker: MockerFixt
351
356
  assert result is None
352
357
 
353
358
 
359
+ def test_ol_config_from_dict():
360
+ # Test with complete config
361
+ config_dict = {
362
+ "transport": {"url": "http://localhost:5050"},
363
+ "facets": {"environment_variables": ["VAR1", "VAR2"]},
364
+ "filters": [{"type": "exact", "match": "job_name"}],
365
+ }
366
+ config = OpenLineageConfig.from_dict(config_dict)
367
+ assert config.transport["url"] == "http://localhost:5050"
368
+ assert config.facets.environment_variables == ["VAR1", "VAR2"]
369
+ assert config.filters[0].type == "exact"
370
+ assert config.filters[0].match == "job_name"
371
+
372
+ # Test with missing keys
373
+ config_dict = {}
374
+ config = OpenLineageConfig.from_dict(config_dict)
375
+ assert config.transport == {}
376
+ assert config.facets == FacetsConfig()
377
+ assert config.filters == []
378
+
379
+ # Test with invalid data type
380
+ with pytest.raises(TypeError):
381
+ OpenLineageConfig.from_dict({"facets": "invalid_data"})
382
+
383
+
354
384
  @patch("yaml.safe_load", return_value=None)
355
385
  def test_config_file_content_empty_file(mock_yaml) -> None: # noqa: ARG001
356
- assert OpenLineageClient().config == {}
386
+ assert OpenLineageClient().config == OpenLineageConfig()
357
387
 
358
388
 
359
389
  def test_config(mocker: MockerFixture, root: Path) -> None:
360
390
  mocker.patch.dict(os.environ, {"OPENLINEAGE_CONFIG": str(root / "config" / "http.yml")})
361
- assert OpenLineageClient().config == {
391
+ assert OpenLineageClient().config == OpenLineageConfig.from_dict(
392
+ {
393
+ "transport": {
394
+ "auth": {"apiKey": "random_token", "type": "api_key"},
395
+ "compression": "gzip",
396
+ "endpoint": "api/v1/lineage",
397
+ "type": "http",
398
+ "url": "http://localhost:5050",
399
+ }
400
+ }
401
+ )
402
+
403
+
404
+ def test_openlineage_client_from_dict() -> None:
405
+ config_dict = {"transport": {"type": "http", "url": "http://localhost:5050"}}
406
+ client = OpenLineageClient.from_dict(config_dict)
407
+ assert client.transport.url == "http://localhost:5050"
408
+
409
+
410
+ def test_openlineage_client_from_empty_dict() -> None:
411
+ client = OpenLineageClient.from_dict({})
412
+ assert isinstance(client.transport, ConsoleTransport)
413
+
414
+
415
+ def test_openlineage_config_from_dict() -> None:
416
+ config_dict = {
362
417
  "transport": {
363
- "auth": {"apiKey": "random_token", "type": "api_key"},
364
- "compression": "gzip",
365
- "endpoint": "api/v1/lineage",
366
418
  "type": "http",
367
419
  "url": "http://localhost:5050",
368
- }
420
+ "auth": {"api_key": "random_token"},
421
+ },
422
+ "facets": {
423
+ "environment_variables": ["VAR1", "VAR2"],
424
+ },
425
+ "filters": [
426
+ {"type": "regex", "match": ".*"},
427
+ ],
369
428
  }
429
+ config = OpenLineageConfig.from_dict(config_dict)
430
+
431
+ assert config.transport == config_dict["transport"]
432
+ assert config.facets.environment_variables == config_dict["facets"]["environment_variables"]
433
+ assert len(config.filters) == 1
434
+ assert config.filters[0].type == "regex"
435
+ assert config.filters[0].match == ".*"
436
+
437
+
438
+ def test_openlineage_config_default_values() -> None:
439
+ config = OpenLineageConfig()
440
+
441
+ assert config.transport == {}
442
+ assert isinstance(config.facets, FacetsConfig)
443
+ assert config.filters == []
444
+
445
+
446
+ @patch.dict(os.environ, {"ENV_VAR_1": "value1", "ENV_VAR_2": "value2"})
447
+ def test_collect_environment_variables():
448
+ client = OpenLineageClient()
449
+ client._config = OpenLineageConfig( # noqa: SLF001
450
+ facets=FacetsConfig(environment_variables=["ENV_VAR_1", "ENV_VAR_2", "MISSING_VAR"])
451
+ )
452
+ env_vars = client._collect_environment_variables() # noqa: SLF001
453
+ assert env_vars == {"ENV_VAR_1": "value1", "ENV_VAR_2": "value2"}
454
+
455
+
456
+ @patch.dict(os.environ, {"ENV_VAR_1": "value1", "SENSITIVE_VAR": "PII"})
457
+ def test_add_environment_facets():
458
+ client = OpenLineageClient()
459
+ client._config = OpenLineageConfig( # noqa: SLF001
460
+ facets=FacetsConfig(environment_variables=["ENV_VAR_1"])
461
+ )
462
+ run = Run(runId=str(generate_new_uuid()))
463
+ event = RunEvent(
464
+ eventType=RunState.START,
465
+ eventTime="2021-11-03T10:53:52.427343",
466
+ run=run,
467
+ job=Job(name="name", namespace=""),
468
+ producer="",
469
+ schemaURL="",
470
+ )
471
+ event.run.facets = {}
472
+
473
+ modified_event = client.add_environment_facets(event)
474
+
475
+ assert "environmentVariables" in modified_event.run.facets
476
+ assert modified_event.run.facets["environmentVariables"] == EnvironmentVariablesRunFacet(
477
+ [EnvironmentVariable(name="ENV_VAR_1", value="value1")]
478
+ )
479
+
480
+
481
+ @patch("openlineage.client.client.OpenLineageClient._find_yaml_config_path")
482
+ @patch("openlineage.client.client.OpenLineageClient._get_config_file_content")
483
+ def test_config_property_loads_yaml(mock_get_config_content, mock_find_yaml):
484
+ mock_find_yaml.return_value = "config.yml"
485
+ mock_get_config_content.return_value = {"transport": {"type": "http", "url": "http://localhost:5050"}}
486
+
487
+ config = OpenLineageClient().config
488
+ assert config.transport["type"] == "http"
489
+ assert config.transport["url"] == "http://localhost:5050"
370
490
 
371
491
 
372
492
  @patch.dict(
@@ -490,6 +610,155 @@ def test_kafka_transport_configured_with_aliased_message_key() -> None:
490
610
  }
491
611
 
492
612
 
613
+ @patch.dict(
614
+ os.environ,
615
+ {
616
+ "CUSTOM_ENV_VAR": "custom_value",
617
+ "OPENLINEAGE__TRANSPORT__TYPE": "console",
618
+ "OPENLINEAGE__FACETS__ENVIRONMENT_VARIABLES": '["CUSTOM_ENV_VAR"]',
619
+ },
620
+ )
621
+ @patch("openlineage.client.client.OpenLineageClient._resolve_transport")
622
+ def test_add_environment_facets_with_custom_env_var(mock_resolve_transport) -> None:
623
+ mock_resolve_transport.return_value = mock_transport = MagicMock()
624
+ client = OpenLineageClient()
625
+ run = Run(runId=str(generate_new_uuid()))
626
+ event = RunEvent(
627
+ eventType=RunState.START,
628
+ eventTime="2021-11-03T10:53:52.427343",
629
+ run=run,
630
+ job=Job(name="name", namespace=""),
631
+ producer="",
632
+ schemaURL="",
633
+ )
634
+
635
+ client.emit(event)
636
+ assert mock_transport.emit.call_args[0][0].run.facets[
637
+ "environmentVariables"
638
+ ] == EnvironmentVariablesRunFacet([EnvironmentVariable(name="CUSTOM_ENV_VAR", value="custom_value")])
639
+
640
+
641
+ @patch.dict(
642
+ os.environ,
643
+ {
644
+ "OPENLINEAGE__TRANSPORT__TYPE": "http",
645
+ "OPENLINEAGE__TRANSPORT__URL": "http://localhost:5050",
646
+ "OPENLINEAGE__TRANSPORT__AUTH__API_KEY": "random_token",
647
+ },
648
+ )
649
+ @patch("openlineage.client.client.OpenLineageClient._find_yaml_config_path")
650
+ def test_config_property_loads_env_vars(mock_find_yaml) -> None:
651
+ mock_find_yaml.return_value = None
652
+ client = OpenLineageClient()
653
+ config = client.config
654
+ assert config.transport["type"] == "http"
655
+ assert config.transport["url"] == "http://localhost:5050"
656
+ assert config.transport["auth"]["api_key"] == "random_token"
657
+
658
+
659
+ def test_config_property_loads_user_defined_config() -> None:
660
+ user_defined_config = {
661
+ "transport": {
662
+ "type": "http",
663
+ "url": "http://localhost:5050",
664
+ "auth": {"api_key": "random_token"},
665
+ }
666
+ }
667
+ client = OpenLineageClient(config=user_defined_config)
668
+ config = client.config
669
+ assert config.transport["type"] == "http"
670
+ assert config.transport["url"] == "http://localhost:5050"
671
+ assert config.transport["auth"]["api_key"] == "random_token"
672
+
673
+
674
+ @patch.dict(
675
+ os.environ,
676
+ {
677
+ "OPENLINEAGE__TRANSPORT__TYPE": "http",
678
+ "OPENLINEAGE__TRANSPORT__URL": "http://localhost:5050",
679
+ },
680
+ )
681
+ def test_client_from_empty_dict_with_dynamic_env_vars() -> None:
682
+ client = OpenLineageClient.from_dict({})
683
+ assert client.transport.url == "http://localhost:5050"
684
+
685
+
686
+ @patch.dict(
687
+ os.environ,
688
+ {
689
+ "OPENLINEAGE_URL": "http://example.com",
690
+ },
691
+ )
692
+ def test_client_from_empty_dict_with_url_env_var() -> None:
693
+ client = OpenLineageClient.from_dict({})
694
+ assert client.transport.url == "http://example.com"
695
+
696
+
697
+ @patch.dict(
698
+ os.environ,
699
+ {
700
+ "OPENLINEAGE__TRANSPORT__TYPE": "http",
701
+ "OPENLINEAGE__TRANSPORT__URL": "http://localhost:5050",
702
+ },
703
+ )
704
+ def test_client_from_facets_config_in_dict_and_env_vars() -> None:
705
+ user_defined_config = {
706
+ "facets": {
707
+ "environment_variables": ["VAR1", "VAR2"],
708
+ }
709
+ }
710
+ client = OpenLineageClient.from_dict(user_defined_config)
711
+ assert client.config.facets.environment_variables == ["VAR1", "VAR2"]
712
+ assert client.transport.url == "http://localhost:5050"
713
+
714
+
715
+ @patch.dict(
716
+ os.environ,
717
+ {
718
+ "OPENLINEAGE__FACETS__ENVIRONMENT_VARIABLES": '["VAR1", "VAR2"]',
719
+ },
720
+ )
721
+ def test_client_from_facets_config_in_env_vars_and_transport_in_config() -> None:
722
+ user_defined_config = {
723
+ "transport": {
724
+ "type": "http",
725
+ "url": "http://localhost:5050",
726
+ }
727
+ }
728
+ client = OpenLineageClient.from_dict(user_defined_config)
729
+ assert client.config.facets.environment_variables == ["VAR1", "VAR2"]
730
+ assert client.transport.url == "http://localhost:5050"
731
+
732
+
733
+ @patch.dict(
734
+ os.environ,
735
+ {
736
+ "OPENLINEAGE__TRANSPORT__AUTH__API_KEY": "random_token",
737
+ },
738
+ )
739
+ @patch("openlineage.client.client.OpenLineageClient._find_yaml_config_path")
740
+ @patch("openlineage.client.client.OpenLineageClient._get_config_file_content")
741
+ def test_config_merge_precedence(mock_get_config_content, mock_find_yaml) -> None:
742
+ user_defined_config = {
743
+ "transport": {
744
+ "type": "http",
745
+ "url": "http://localhost:5050",
746
+ }
747
+ }
748
+ mock_find_yaml.return_value = "config.yml"
749
+ mock_get_config_content.return_value = {
750
+ "transport": {
751
+ "url": "http://another.host:5050",
752
+ "auth": {"api_key": "another_token"},
753
+ }
754
+ }
755
+ client = OpenLineageClient.from_dict(user_defined_config)
756
+ config = client.config
757
+ assert config.transport["type"] == "http"
758
+ assert config.transport["url"] == "http://localhost:5050"
759
+ assert config.transport["auth"]["api_key"] == "another_token"
760
+
761
+
493
762
  class TestOpenLineageConfigLoader:
494
763
  @pytest.mark.parametrize(
495
764
  ("env_vars", "expected_config"),
@@ -0,0 +1,65 @@
1
+ # Copyright 2018-2024 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from openlineage.client.utils import deep_merge_dicts, import_from_string, try_import_from_string
7
+
8
+
9
+ def test_import_from_string():
10
+ from openlineage.client.filter import ExactMatchFilter
11
+
12
+ result = import_from_string("openlineage.client.filter.ExactMatchFilter")
13
+ assert result == ExactMatchFilter
14
+
15
+
16
+ def test_import_from_string_unknown_path():
17
+ with pytest.raises(ImportError):
18
+ import_from_string("openlineage.client.non-existing-module")
19
+
20
+
21
+ def test_try_import_from_string():
22
+ from openlineage.client.filter import ExactMatchFilter
23
+
24
+ result = try_import_from_string("openlineage.client.filter.ExactMatchFilter")
25
+ assert result == ExactMatchFilter
26
+
27
+
28
+ def test_try_import_from_string_unknown():
29
+ result = try_import_from_string("openlineage.client.non-existing-module")
30
+ assert result is None
31
+
32
+
33
+ def test_deep_merge_dicts_simple():
34
+ dict1 = {"a": 1, "b": 2}
35
+ dict2 = {"b": 3, "c": 4}
36
+ expected = {"a": 1, "b": 3, "c": 4}
37
+ assert deep_merge_dicts(dict1, dict2) == expected
38
+
39
+
40
+ def test_deep_merge_dicts_nested():
41
+ dict1 = {"a": {"x": 1}, "b": 2}
42
+ dict2 = {"a": {"y": 2}, "b": 3, "c": 4}
43
+ expected = {"a": {"x": 1, "y": 2}, "b": 3, "c": 4}
44
+ assert deep_merge_dicts(dict1, dict2) == expected
45
+
46
+
47
+ def test_deep_merge_dicts_overwrite():
48
+ dict1 = {"a": {"x": 1}}
49
+ dict2 = {"a": {"x": 2}}
50
+ expected = {"a": {"x": 2}}
51
+ assert deep_merge_dicts(dict1, dict2) == expected
52
+
53
+
54
+ def test_deep_merge_dicts_non_dict_values():
55
+ dict1 = {"a": 1, "b": {"x": 1}}
56
+ dict2 = {"b": 2, "c": 3}
57
+ expected = {"a": 1, "b": 2, "c": 3}
58
+ assert deep_merge_dicts(dict1, dict2) == expected
59
+
60
+
61
+ def test_deep_merge_dicts_empty_dicts():
62
+ dict1 = {}
63
+ dict2 = {"a": 1}
64
+ expected = {"a": 1}
65
+ assert deep_merge_dicts(dict1, dict2) == expected
@@ -1,30 +0,0 @@
1
- # Copyright 2018-2024 contributors to the OpenLineage project
2
- # SPDX-License-Identifier: Apache-2.0
3
- from __future__ import annotations
4
-
5
- import pytest
6
- from openlineage.client.utils import import_from_string, try_import_from_string
7
-
8
-
9
- def test_import_from_string():
10
- from openlineage.client.filter import ExactMatchFilter
11
-
12
- result = import_from_string("openlineage.client.filter.ExactMatchFilter")
13
- assert result == ExactMatchFilter
14
-
15
-
16
- def test_import_from_string_unknown_path():
17
- with pytest.raises(ImportError):
18
- import_from_string("openlineage.client.non-existing-module")
19
-
20
-
21
- def test_try_import_from_string():
22
- from openlineage.client.filter import ExactMatchFilter
23
-
24
- result = try_import_from_string("openlineage.client.filter.ExactMatchFilter")
25
- assert result == ExactMatchFilter
26
-
27
-
28
- def test_try_import_from_string_unknown():
29
- result = try_import_from_string("openlineage.client.non-existing-module")
30
- assert result is None