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.
Files changed (88) hide show
  1. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/PKG-INFO +1 -1
  2. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/client.py +165 -14
  3. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/constants.py +1 -1
  4. {openlineage_python-1.22.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.22.0 → openlineage_python-1.24.0}/openlineage/client/filter.py +14 -6
  7. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/column_lineage_dataset.py +13 -1
  8. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/datasource_dataset.py +2 -0
  9. openlineage_python-1.24.0/openlineage/client/generated/environment_variables_run.py +35 -0
  10. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/nominal_time_run.py +2 -0
  11. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/composite.py +7 -2
  12. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/factory.py +6 -1
  13. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/http.py +7 -0
  14. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/kafka.py +3 -0
  15. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/transport.py +4 -0
  16. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/utils.py +19 -0
  17. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/pyproject.toml +1 -1
  18. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/redact_fields.yml +7 -0
  19. openlineage_python-1.24.0/tests/test_client.py +853 -0
  20. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_composite.py +10 -0
  21. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_facet_v2.py +5 -0
  22. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_http.py +119 -1
  23. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_kafka.py +16 -0
  24. openlineage_python-1.24.0/tests/test_utils.py +65 -0
  25. openlineage_python-1.22.0/tests/test_client.py +0 -391
  26. openlineage_python-1.22.0/tests/test_utils.py +0 -30
  27. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/.gitignore +0 -0
  28. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/README.md +0 -0
  29. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/Makefile +0 -0
  30. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/conf.py +0 -0
  31. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/convert.py +0 -0
  32. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/index.rst +0 -0
  33. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/make.bat +0 -0
  34. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/docs/source/openlineage.client.rst +0 -0
  35. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/__init__.py +0 -0
  36. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/event_v2.py +0 -0
  37. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/facet.py +0 -0
  38. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/base.py +0 -0
  39. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/data_quality_assertions_dataset.py +0 -0
  40. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/data_quality_metrics_input_dataset.py +0 -0
  41. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/dataset_version_dataset.py +0 -0
  42. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/documentation_dataset.py +0 -0
  43. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/documentation_job.py +0 -0
  44. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/error_message_run.py +0 -0
  45. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/external_query_run.py +0 -0
  46. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/extraction_error_run.py +0 -0
  47. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/job_type_job.py +0 -0
  48. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/lifecycle_state_change_dataset.py +0 -0
  49. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/output_statistics_output_dataset.py +0 -0
  50. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/ownership_dataset.py +0 -0
  51. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/ownership_job.py +0 -0
  52. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/parent_run.py +0 -0
  53. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/processing_engine_run.py +0 -0
  54. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/schema_dataset.py +0 -0
  55. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/source_code_job.py +0 -0
  56. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/source_code_location_job.py +0 -0
  57. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/sql_job.py +0 -0
  58. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/storage_dataset.py +0 -0
  59. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/generated/symlinks_dataset.py +0 -0
  60. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/py.typed +0 -0
  61. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/run.py +0 -0
  62. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/serde.py +0 -0
  63. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/__init__.py +0 -0
  64. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/console.py +0 -0
  65. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/file.py +0 -0
  66. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/msk_iam.py +0 -0
  67. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/transport/noop.py +0 -0
  68. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/openlineage/client/uuid.py +0 -0
  69. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/__init__.py +0 -0
  70. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/config.yml +0 -0
  71. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/exact_filter.yml +0 -0
  72. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/http.yml +0 -0
  73. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/openlineage.yml +0 -0
  74. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/config/regex_filter.yml +0 -0
  75. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/conftest.py +0 -0
  76. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/example_full_event.json +0 -0
  77. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/nominal_time_without_end.json +0 -0
  78. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/serde_example_dataset_event.json +0 -0
  79. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/serde_example_job_event.json +0 -0
  80. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/serde_example_run_event.json +0 -0
  81. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_events.py +0 -0
  82. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_facet.py +0 -0
  83. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_factory.py +0 -0
  84. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_file.py +0 -0
  85. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_msk_iam.py +0 -0
  86. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/test_uuid.py +0 -0
  87. {openlineage_python-1.22.0 → openlineage_python-1.24.0}/tests/transport.py +0 -0
  88. {openlineage_python-1.22.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.22.0
3
+ Version: 1.24.0
4
4
  Summary: OpenLineage Python Client
5
5
  Author-email: OpenLineage <info@openlineage.io>
6
6
  Keywords: openlineage
@@ -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 Transport, TransportFactory, get_default_factory
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: dict[str, dict[str, str]] | None = None
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.get("filters", []):
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(transport=get_default_factory().create(config=config))
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) -> dict[str, Any]:
125
- """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
+ """
126
182
  if self._config is None:
127
- config_path = self._find_yaml_config_path()
128
- if config_path:
129
- self._config = self._get_config_file_content(config_path)
130
- else:
131
- 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
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("transport"):
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["transport"])
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
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.22.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
@@ -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-1-0/ColumnLineageDatasetFacet.json#/$defs/ColumnLineageDatasetFacet"
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):
@@ -21,6 +21,8 @@ class DatasourceDatasetFacet(DatasetFacet):
21
21
 
22
22
  @uri.validator
23
23
  def uri_check(self, attribute: str, value: str) -> None: # noqa: ARG002
24
+ if value is None:
25
+ return
24
26
  from urllib.parse import urlparse
25
27
 
26
28
  urlparse(value)
@@ -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"
@@ -39,6 +39,8 @@ class NominalTimeRunFacet(RunFacet):
39
39
 
40
40
  @nominalEndTime.validator
41
41
  def nominalendtime_check(self, attribute: str, value: str) -> None: # noqa: ARG002
42
+ if value is None:
43
+ return
42
44
  from dateutil import parser
43
45
 
44
46
  parser.isoparse(value)
@@ -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
- for transport_config in self.config.transports:
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
 
@@ -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
- return transport_class(config_class.from_dict(config)) # type: ignore[call-arg]
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
@@ -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),
@@ -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)
@@ -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
 
@@ -6,7 +6,7 @@ requires = [
6
6
 
7
7
  [project]
8
8
  name = "openlineage-python"
9
- version = "1.22.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