openlineage-python 1.30.1__tar.gz → 1.32.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 (105) hide show
  1. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/PKG-INFO +8 -4
  2. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/client.py +10 -5
  3. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/constants.py +1 -1
  4. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/facet.py +26 -26
  5. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/filter.py +8 -1
  6. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/parent_run.py +42 -1
  7. openlineage_python-1.32.0/openlineage/client/generator/base.py +257 -0
  8. openlineage_python-1.32.0/openlineage/client/generator/cli.py +53 -0
  9. openlineage_python-1.32.0/openlineage/client/generator/generate.py +100 -0
  10. openlineage_python-1.32.0/openlineage/client/generator/templates/Enum.jinja2 +22 -0
  11. openlineage_python-1.32.0/openlineage/client/generator/templates/dataclass.jinja2 +196 -0
  12. openlineage_python-1.32.0/openlineage/client/generator/templates/facet_v2.jinja2 +25 -0
  13. openlineage_python-1.32.0/openlineage/client/generator/templates/root.jinja2 +1 -0
  14. openlineage_python-1.32.0/openlineage/client/generator/templates/validators.jinja2 +43 -0
  15. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/run.py +16 -16
  16. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/serde.py +4 -4
  17. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/composite.py +10 -5
  18. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/console.py +4 -3
  19. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/http.py +34 -20
  20. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/kafka.py +41 -19
  21. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/transport.py +3 -0
  22. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/utils.py +2 -2
  23. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/pyproject.toml +13 -4
  24. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/redact_fields.yml +11 -0
  25. openlineage_python-1.32.0/tests/__init__.py +2 -0
  26. openlineage_python-1.32.0/tests/generator/__init__.py +2 -0
  27. openlineage_python-1.32.0/tests/generator/test_base.py +148 -0
  28. openlineage_python-1.32.0/tests/generator/test_cli.py +53 -0
  29. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_client.py +71 -5
  30. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_file.py +3 -3
  31. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_http.py +70 -3
  32. openlineage_python-1.32.0/tests/test_kafka.py +477 -0
  33. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tox.ini +2 -1
  34. openlineage_python-1.30.1/tests/test_kafka.py +0 -338
  35. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/.gitignore +0 -0
  36. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/README.md +0 -0
  37. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/docs/Makefile +0 -0
  38. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/docs/conf.py +0 -0
  39. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/docs/convert.py +0 -0
  40. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/docs/index.rst +0 -0
  41. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/docs/make.bat +0 -0
  42. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/docs/source/openlineage.client.rst +0 -0
  43. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/__init__.py +0 -0
  44. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/event_v2.py +0 -0
  45. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/facet_v2.py +0 -0
  46. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/facets.py +0 -0
  47. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/base.py +0 -0
  48. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/column_lineage_dataset.py +0 -0
  49. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/data_quality_assertions_dataset.py +0 -0
  50. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/data_quality_metrics_input_dataset.py +0 -0
  51. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/dataset_type_dataset.py +0 -0
  52. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/dataset_version_dataset.py +0 -0
  53. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/datasource_dataset.py +0 -0
  54. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/documentation_dataset.py +0 -0
  55. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/documentation_job.py +0 -0
  56. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/environment_variables_run.py +0 -0
  57. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/error_message_run.py +0 -0
  58. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/external_query_run.py +0 -0
  59. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/extraction_error_run.py +0 -0
  60. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/input_statistics_input_dataset.py +0 -0
  61. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/job_type_job.py +0 -0
  62. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/lifecycle_state_change_dataset.py +0 -0
  63. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/nominal_time_run.py +0 -0
  64. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/output_statistics_output_dataset.py +0 -0
  65. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/ownership_dataset.py +0 -0
  66. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/ownership_job.py +0 -0
  67. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/processing_engine_run.py +0 -0
  68. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/schema_dataset.py +0 -0
  69. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/source_code_job.py +0 -0
  70. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/source_code_location_job.py +0 -0
  71. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/sql_job.py +0 -0
  72. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/storage_dataset.py +0 -0
  73. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/symlinks_dataset.py +0 -0
  74. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/tags_dataset.py +0 -0
  75. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/tags_job.py +0 -0
  76. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/generated/tags_run.py +0 -0
  77. openlineage_python-1.30.1/tests/__init__.py → openlineage_python-1.32.0/openlineage/client/generator/header.py +0 -0
  78. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/py.typed +0 -0
  79. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/tags.py +0 -0
  80. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/__init__.py +0 -0
  81. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/factory.py +0 -0
  82. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/file.py +1 -1
  83. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/msk_iam.py +0 -0
  84. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/transport/noop.py +0 -0
  85. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/openlineage/client/uuid.py +0 -0
  86. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/config/config.yml +0 -0
  87. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/config/exact_filter.yml +0 -0
  88. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/config/http.yml +0 -0
  89. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/config/openlineage.yml +0 -0
  90. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/config/regex_filter.yml +0 -0
  91. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/conftest.py +0 -0
  92. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/example_full_event.json +0 -0
  93. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/nominal_time_without_end.json +0 -0
  94. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/serde_example_dataset_event.json +0 -0
  95. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/serde_example_job_event.json +0 -0
  96. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/serde_example_run_event.json +0 -0
  97. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_composite.py +0 -0
  98. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_events.py +0 -0
  99. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_facet.py +0 -0
  100. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_facet_v2.py +0 -0
  101. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_factory.py +0 -0
  102. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_msk_iam.py +0 -0
  103. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_utils.py +0 -0
  104. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/test_uuid.py +0 -0
  105. {openlineage_python-1.30.1 → openlineage_python-1.32.0}/tests/transport.py +0 -0
@@ -1,28 +1,32 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openlineage-python
3
- Version: 1.30.1
3
+ Version: 1.32.0
4
4
  Summary: OpenLineage Python Client
5
5
  Author-email: OpenLineage <info@openlineage.io>
6
6
  Keywords: openlineage
7
7
  Classifier: Operating System :: OS Independent
8
8
  Classifier: Programming Language :: Python
9
9
  Classifier: Programming Language :: Python :: 3 :: Only
10
- Classifier: Programming Language :: Python :: 3.8
11
10
  Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
15
- Requires-Python: >=3.8
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.9
16
16
  Requires-Dist: attrs>=20.0
17
17
  Requires-Dist: packaging>=21.0
18
18
  Requires-Dist: python-dateutil>=2.8.2
19
19
  Requires-Dist: pyyaml>=5.4
20
- Requires-Dist: requests>=2.20.0
20
+ Requires-Dist: requests>=2.32.3
21
21
  Provides-Extra: docs
22
22
  Requires-Dist: beautifulsoup4>=4.12.0; extra == 'docs'
23
23
  Requires-Dist: pydata-sphinx-theme>=0.13; extra == 'docs'
24
24
  Requires-Dist: sphinx-autodoc-typehints>=2.0.0; extra == 'docs'
25
25
  Requires-Dist: sphinx>=7.0; extra == 'docs'
26
+ Provides-Extra: generator
27
+ Requires-Dist: click; extra == 'generator'
28
+ Requires-Dist: datamodel-code-generator==0.25.4; extra == 'generator'
29
+ Requires-Dist: ruff; extra == 'generator'
26
30
  Provides-Extra: kafka
27
31
  Requires-Dist: confluent-kafka>=2.1.1; extra == 'kafka'
28
32
  Provides-Extra: msk-iam
@@ -153,7 +153,7 @@ class OpenLineageClient:
153
153
  ) -> Event | None:
154
154
  """Filters jobs according to config-defined events"""
155
155
  for _filter in self._filters:
156
- if isinstance(event, RunEvent) and _filter.filter_event(event) is None:
156
+ if isinstance(event, (RunEvent, event_v2.RunEvent)) and _filter.filter_event(event) is None:
157
157
  return None
158
158
  return event
159
159
 
@@ -163,6 +163,11 @@ class OpenLineageClient:
163
163
  if type(event) not in get_args(Event):
164
164
  msg = "`emit` only accepts RunEvent, DatasetEvent, JobEvent classes"
165
165
  raise ValueError(msg)
166
+
167
+ if log.isEnabledFor(logging.DEBUG):
168
+ val = Serde.to_json(event).encode("utf-8")
169
+ log.debug("OpenLineageClient will *try* to emit event %s", val)
170
+
166
171
  if not self.transport:
167
172
  log.error("Tried to emit OpenLineage event, but transport is not configured.")
168
173
  return
@@ -176,9 +181,6 @@ class OpenLineageClient:
176
181
  event = self.add_environment_facets(event)
177
182
  event = self.update_event_tags_facets(event)
178
183
 
179
- if log.isEnabledFor(logging.DEBUG):
180
- val = Serde.to_json(event).encode("utf-8")
181
- log.debug("OpenLineageClient will emit event %s", val)
182
184
  self.transport.emit(event)
183
185
  log.debug("OpenLineage event successfully emitted.")
184
186
 
@@ -395,7 +397,10 @@ class OpenLineageClient:
395
397
  """
396
398
  Adds environment variables as facets to the event object.
397
399
  """
398
- if isinstance(event, RunEvent) and (env_vars := self._collect_environment_variables()):
400
+ if isinstance(event, (RunEvent, event_v2.RunEvent)) and (
401
+ env_vars := self._collect_environment_variables()
402
+ ):
403
+ event.run.facets = event.run.facets or {}
399
404
  event.run.facets["environmentVariables"] = EnvironmentVariablesRunFacet(
400
405
  environmentVariables=[
401
406
  EnvironmentVariable(name=name, value=value) for name, value in env_vars.items()
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "1.30.1"
5
+ __version__ = "1.32.0"
6
6
 
7
7
  DEFAULT_TIMEOUT_MS = 5000
8
8
  DEFAULT_NAMESPACE_NAME = "default"
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  import warnings
4
4
  from enum import Enum
5
- from typing import Any, ClassVar, Dict, List, Optional
5
+ from typing import Any, ClassVar, Optional
6
6
 
7
7
  import attr
8
8
  from openlineage.client.constants import DEFAULT_PRODUCER
@@ -27,8 +27,8 @@ class BaseFacet(RedactMixin):
27
27
  _producer: str = attr.ib(init=False)
28
28
  _schemaURL: str = attr.ib(init=False) # noqa: N815
29
29
 
30
- _base_skip_redact: ClassVar[List[str]] = ["_producer", "_schemaURL"]
31
- _additional_skip_redact: ClassVar[List[str]] = []
30
+ _base_skip_redact: ClassVar[list[str]] = ["_producer", "_schemaURL"]
31
+ _additional_skip_redact: ClassVar[list[str]] = []
32
32
 
33
33
  def __attrs_post_init__(self) -> None:
34
34
  self._producer = PRODUCER
@@ -39,7 +39,7 @@ class BaseFacet(RedactMixin):
39
39
  return SCHEMA_URI + "#/definitions/BaseFacet"
40
40
 
41
41
  @property
42
- def skip_redact(self) -> List[str]:
42
+ def skip_redact(self) -> list[str]:
43
43
  return self._base_skip_redact + self._additional_skip_redact
44
44
 
45
45
 
@@ -48,7 +48,7 @@ class NominalTimeRunFacet(BaseFacet):
48
48
  nominalStartTime: str = attr.ib() # noqa: N815
49
49
  nominalEndTime: Optional[str] = attr.ib(default=None) # noqa: N815
50
50
 
51
- _additional_skip_redact: ClassVar[List[str]] = ["nominalStartTime", "nominalEndTime"]
51
+ _additional_skip_redact: ClassVar[list[str]] = ["nominalStartTime", "nominalEndTime"]
52
52
 
53
53
  @staticmethod
54
54
  def _get_schema() -> str:
@@ -57,10 +57,10 @@ class NominalTimeRunFacet(BaseFacet):
57
57
 
58
58
  @attr.s
59
59
  class ParentRunFacet(BaseFacet):
60
- run: Dict[Any, Any] = attr.ib()
61
- job: Dict[Any, Any] = attr.ib()
60
+ run: dict[Any, Any] = attr.ib()
61
+ job: dict[Any, Any] = attr.ib()
62
62
 
63
- _additional_skip_redact: ClassVar[List[str]] = ["job", "run"]
63
+ _additional_skip_redact: ClassVar[list[str]] = ["job", "run"]
64
64
 
65
65
  @classmethod
66
66
  def create(cls, runId: str, namespace: str, name: str) -> "ParentRunFacet": # noqa: N803
@@ -93,7 +93,7 @@ class SourceCodeLocationJobFacet(BaseFacet):
93
93
  type: str = attr.ib()
94
94
  url: str = attr.ib()
95
95
 
96
- _additional_skip_redact: ClassVar[List[str]] = ["type", "url"]
96
+ _additional_skip_redact: ClassVar[list[str]] = ["type", "url"]
97
97
 
98
98
  @staticmethod
99
99
  def _get_schema() -> str:
@@ -124,12 +124,12 @@ class SchemaField(RedactMixin):
124
124
  type: str = attr.ib()
125
125
  description: Optional[str] = attr.ib(default=None)
126
126
 
127
- _do_not_redact: ClassVar[List[str]] = ["name", "type"]
127
+ _do_not_redact: ClassVar[list[str]] = ["name", "type"]
128
128
 
129
129
 
130
130
  @attr.s
131
131
  class SchemaDatasetFacet(BaseFacet):
132
- fields: List[SchemaField] = attr.ib()
132
+ fields: list[SchemaField] = attr.ib()
133
133
 
134
134
  @staticmethod
135
135
  def _get_schema() -> str:
@@ -141,7 +141,7 @@ class DataSourceDatasetFacet(BaseFacet):
141
141
  name: str = attr.ib()
142
142
  uri: str = attr.ib()
143
143
 
144
- _additional_skip_redact: ClassVar[List[str]] = ["name", "uri"]
144
+ _additional_skip_redact: ClassVar[list[str]] = ["name", "uri"]
145
145
 
146
146
  @staticmethod
147
147
  def _get_schema() -> str:
@@ -154,7 +154,7 @@ class OutputStatisticsOutputDatasetFacet(BaseFacet):
154
154
  size: Optional[int] = attr.ib(default=None)
155
155
  fileCount: Optional[int] = attr.ib(default=None) # noqa: N815
156
156
 
157
- _additional_skip_redact: ClassVar[List[str]] = ["rowCount", "size", "fileCount"]
157
+ _additional_skip_redact: ClassVar[list[str]] = ["rowCount", "size", "fileCount"]
158
158
 
159
159
  @staticmethod
160
160
  def _get_schema() -> str:
@@ -169,7 +169,7 @@ class ColumnMetric:
169
169
  count: Optional[int] = attr.ib(default=None)
170
170
  min: Optional[float] = attr.ib(default=None)
171
171
  max: Optional[float] = attr.ib(default=None)
172
- quantiles: Optional[Dict[str, float]] = attr.ib(default=None)
172
+ quantiles: Optional[dict[str, float]] = attr.ib(default=None)
173
173
 
174
174
 
175
175
  @attr.s
@@ -177,7 +177,7 @@ class DataQualityMetricsInputDatasetFacet(BaseFacet):
177
177
  rowCount: Optional[int] = attr.ib(default=None) # noqa: N815
178
178
  bytes: Optional[int] = attr.ib(default=None)
179
179
  fileCount: Optional[int] = attr.ib(default=None) # noqa: N815
180
- columnMetrics: Dict[str, ColumnMetric] = attr.ib(factory=dict) # noqa: N815
180
+ columnMetrics: dict[str, ColumnMetric] = attr.ib(factory=dict) # noqa: N815
181
181
 
182
182
  @staticmethod
183
183
  def _get_schema() -> str:
@@ -190,7 +190,7 @@ class Assertion(RedactMixin):
190
190
  success: bool = attr.ib()
191
191
  column: Optional[str] = attr.ib(default=None)
192
192
 
193
- _skip_redact: ClassVar[List[str]] = ["column"]
193
+ _skip_redact: ClassVar[list[str]] = ["column"]
194
194
 
195
195
 
196
196
  @attr.s
@@ -198,7 +198,7 @@ class DataQualityAssertionsDatasetFacet(BaseFacet):
198
198
 
199
199
  """This facet represents asserted expectations on dataset or it's column."""
200
200
 
201
- assertions: List[Assertion] = attr.ib()
201
+ assertions: list[Assertion] = attr.ib()
202
202
 
203
203
  @staticmethod
204
204
  def _get_schema() -> str:
@@ -213,7 +213,7 @@ class SourceCodeJobFacet(BaseFacet):
213
213
  language: str = attr.ib() # language that the code was written in
214
214
  source: str = attr.ib() # source code text
215
215
 
216
- _additional_skip_redact: ClassVar[List[str]] = ["language"]
216
+ _additional_skip_redact: ClassVar[list[str]] = ["language"]
217
217
 
218
218
  @staticmethod
219
219
  def _get_schema() -> str:
@@ -235,7 +235,7 @@ class ErrorMessageRunFacet(BaseFacet):
235
235
  programmingLanguage: str = attr.ib() # noqa: N815
236
236
  stackTrace: Optional[str] = attr.ib(default=None) # noqa: N815
237
237
 
238
- _additional_skip_redact: ClassVar[List[str]] = ["programmingLanguage"]
238
+ _additional_skip_redact: ClassVar[list[str]] = ["programmingLanguage"]
239
239
 
240
240
  @staticmethod
241
241
  def _get_schema() -> str:
@@ -254,7 +254,7 @@ class SymlinksDatasetFacet(BaseFacet):
254
254
 
255
255
  """This facet represents dataset symlink names."""
256
256
 
257
- identifiers: List[SymlinksDatasetFacetIdentifiers] = attr.ib(factory=dict)
257
+ identifiers: list[SymlinksDatasetFacetIdentifiers] = attr.ib(factory=dict)
258
258
 
259
259
  @staticmethod
260
260
  def _get_schema() -> str:
@@ -285,7 +285,7 @@ class OwnershipJobFacet(BaseFacet):
285
285
 
286
286
  """This facet represents ownership of a job."""
287
287
 
288
- owners: List[OwnershipJobFacetOwners] = attr.ib(factory=dict)
288
+ owners: list[OwnershipJobFacetOwners] = attr.ib(factory=dict)
289
289
 
290
290
  @staticmethod
291
291
  def _get_schema() -> str:
@@ -357,7 +357,7 @@ class OwnershipDatasetFacet(BaseFacet):
357
357
 
358
358
  """This facet represents ownership of a dataset."""
359
359
 
360
- owners: List[OwnershipDatasetFacetOwners] = attr.ib(factory=dict)
360
+ owners: list[OwnershipDatasetFacetOwners] = attr.ib(factory=dict)
361
361
 
362
362
  @staticmethod
363
363
  def _get_schema() -> str:
@@ -370,12 +370,12 @@ class ColumnLineageDatasetFacetFieldsAdditionalInputFields(RedactMixin):
370
370
  name: str = attr.ib()
371
371
  field: str = attr.ib()
372
372
 
373
- _skip_redact: ClassVar[List[str]] = ["namespace", "name", "field"]
373
+ _skip_redact: ClassVar[list[str]] = ["namespace", "name", "field"]
374
374
 
375
375
 
376
376
  @attr.s
377
377
  class ColumnLineageDatasetFacetFieldsAdditional:
378
- inputFields: ClassVar[List[ColumnLineageDatasetFacetFieldsAdditionalInputFields]] = attr.ib() # noqa: N815
378
+ inputFields: ClassVar[list[ColumnLineageDatasetFacetFieldsAdditionalInputFields]] = attr.ib() # noqa: N815
379
379
  transformationDescription: str = attr.ib() # noqa: N815
380
380
  transformationType: str = attr.ib() # noqa: N815
381
381
 
@@ -385,7 +385,7 @@ class ColumnLineageDatasetFacet(BaseFacet):
385
385
 
386
386
  """This facet contains column lineage of a dataset."""
387
387
 
388
- fields: Dict[str, ColumnLineageDatasetFacetFieldsAdditional] = attr.ib(factory=dict)
388
+ fields: dict[str, ColumnLineageDatasetFacetFieldsAdditional] = attr.ib(factory=dict)
389
389
 
390
390
  @staticmethod
391
391
  def _get_schema() -> str:
@@ -415,7 +415,7 @@ class ExtractionError(BaseFacet):
415
415
  class ExtractionErrorRunFacet(BaseFacet):
416
416
  totalTasks: int = attr.ib() # noqa: N815
417
417
  failedTasks: int = attr.ib() # noqa: N815
418
- errors: List[ExtractionError] = attr.ib()
418
+ errors: list[ExtractionError] = attr.ib()
419
419
 
420
420
  @staticmethod
421
421
  def _get_schema() -> str:
@@ -2,6 +2,7 @@
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  import re
6
7
  import typing
7
8
 
@@ -9,6 +10,7 @@ import attr
9
10
  from openlineage.client.event_v2 import RunEvent as RunEvent_v2
10
11
  from openlineage.client.run import RunEvent
11
12
 
13
+ log = logging.getLogger(__name__)
12
14
  RunEventType = typing.Union[RunEvent, RunEvent_v2]
13
15
 
14
16
 
@@ -30,6 +32,7 @@ class ExactMatchFilter(Filter):
30
32
 
31
33
  def filter_event(self, event: RunEventType) -> RunEventType | None:
32
34
  if self.match == event.job.name:
35
+ log.debug("Job name `%s` matches exactly `%s`.", event.job.name, self.match)
33
36
  return None
34
37
  return event
35
38
 
@@ -40,16 +43,20 @@ class RegexFilter(Filter):
40
43
 
41
44
  def filter_event(self, event: RunEventType) -> RunEventType | None:
42
45
  if self.pattern.match(event.job.name):
46
+ log.debug("Job name `%s` matches regex `%s`.", event.job.name, self.pattern.pattern)
43
47
  return None
44
48
  return event
45
49
 
46
50
 
47
51
  def create_filter(conf: FilterConfig) -> Filter | None:
48
52
  if not conf.type:
53
+ log.warning("OpenLineage filter config must have a `type`.")
49
54
  return None
50
- # Switch in 3.10 🙂
51
55
  if conf.type == "exact" and conf.match:
56
+ log.debug("Creating ExactMatchFilter with match='%s'", conf.match)
52
57
  return ExactMatchFilter(match=conf.match)
53
58
  if conf.type == "regex" and conf.regex:
59
+ log.debug("Creating RegexFilter with regex='%s'", conf.regex)
54
60
  return RegexFilter(regex=conf.regex)
61
+ log.warning("Unsupported OpenLineage filter type: `%s`", conf.type)
55
62
  return None
@@ -28,11 +28,12 @@ class ParentRunFacet(RunFacet):
28
28
 
29
29
  run: Run
30
30
  job: Job
31
+ root: Root | None = attr.field(default=None)
31
32
  _additional_skip_redact: ClassVar[list[str]] = ["job", "run"]
32
33
 
33
34
  @staticmethod
34
35
  def _get_schema() -> str:
35
- return "https://openlineage.io/spec/facets/1-0-1/ParentRunFacet.json#/$defs/ParentRunFacet"
36
+ return "https://openlineage.io/spec/facets/1-1-0/ParentRunFacet.json#/$defs/ParentRunFacet"
36
37
 
37
38
  @classmethod
38
39
  def create(cls, runId: str, namespace: str, name: str) -> ParentRunFacet: # noqa: N803
@@ -46,6 +47,46 @@ class ParentRunFacet(RunFacet):
46
47
  return cls(run=Run(runId=runId), job=Job(namespace=namespace, name=name))
47
48
 
48
49
 
50
+ @attr.define
51
+ class Root(RedactMixin):
52
+ run: RootRun
53
+ job: RootJob
54
+ _skip_redact: ClassVar[list[str]] = ["run", "job"]
55
+
56
+
57
+ @attr.define
58
+ class RootJob(RedactMixin):
59
+ namespace: str
60
+ """The namespace containing root job"""
61
+
62
+ name: str
63
+ """The unique name containing root job within that namespace"""
64
+
65
+ _skip_redact: ClassVar[list[str]] = ["namespace", "name"]
66
+
67
+ @staticmethod
68
+ def _get_schema() -> str:
69
+ return "https://openlineage.io/spec/facets/1-1-0/ParentRunFacet.json#/$defs/RootJob"
70
+
71
+
72
+ @attr.define
73
+ class RootRun(RedactMixin):
74
+ runId: str = attr.field() # noqa: N815
75
+ """The globally unique ID of the root run associated with the root job."""
76
+
77
+ _skip_redact: ClassVar[list[str]] = ["runId"]
78
+
79
+ @staticmethod
80
+ def _get_schema() -> str:
81
+ return "https://openlineage.io/spec/facets/1-1-0/ParentRunFacet.json#/$defs/RootRun"
82
+
83
+ @runId.validator
84
+ def runid_check(self, attribute: str, value: str) -> None: # noqa: ARG002
85
+ from uuid import UUID
86
+
87
+ UUID(value)
88
+
89
+
49
90
  @attr.define
50
91
  class Run(RedactMixin):
51
92
  runId: str = attr.field() # noqa: N815
@@ -0,0 +1,257 @@
1
+ # Copyright 2018-2025 contributors to the OpenLineage project
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import json
7
+ import logging
8
+ import os
9
+ import pathlib
10
+ import re
11
+ import subprocess
12
+ import tempfile
13
+ from typing import Any
14
+
15
+ from datamodel_code_generator import DataModelType, PythonVersion
16
+ from datamodel_code_generator.imports import Import
17
+ from datamodel_code_generator.model import get_data_model_types
18
+ from datamodel_code_generator.model import pydantic as pydantic_model
19
+ from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
20
+ from datamodel_code_generator.types import Types
21
+
22
+ log = logging.getLogger(__name__)
23
+
24
+
25
+ def camel_to_snake(string: str) -> str:
26
+ """Convert camel_case string to SnakeCase"""
27
+ _under_score_1 = re.compile(r"([^_])([A-Z][a-z]+)")
28
+ _under_score_2 = re.compile("([a-z0-9])([A-Z])")
29
+ subbed = _under_score_1.sub(r"\1_\2", string)
30
+ return _under_score_2.sub(r"\1_\2", subbed).lower()
31
+
32
+
33
+ # import attrs instead of dataclass
34
+ DATA_MODELS_TYPES = get_data_model_types(
35
+ DataModelType.DataclassesDataclass, target_python_version=PythonVersion.PY_38
36
+ )
37
+ NEW_MODEL = DATA_MODELS_TYPES.data_model
38
+ NEW_MODEL.DEFAULT_IMPORTS = (Import.from_full_path("attr"),)
39
+
40
+ # custom code
41
+ SET_PRODUCER_CODE = """
42
+ PRODUCER = DEFAULT_PRODUCER
43
+
44
+ def set_producer(producer: str) -> None:
45
+ global PRODUCER # noqa: PLW0603
46
+ PRODUCER = producer
47
+ """
48
+
49
+ # locations definitions
50
+ FILE_LOCATION = pathlib.Path(__file__).resolve().parent
51
+ TEMPLATES_LOCATION = FILE_LOCATION / "templates"
52
+
53
+ # Global dictionaries intended to be imported by other modules
54
+ # Contains schema URLs and base IDs parsed from specification files
55
+ SCHEMA_URLS: dict[str, str] = {}
56
+ BASE_IDS: dict[str, str] = {}
57
+
58
+
59
+ def deep_merge_dicts(dict1: dict[str, Any], dict2: dict[str, Any]) -> dict[str, Any]:
60
+ """Deep merges two dictionaries.
61
+
62
+ This function merges two dictionaries while handling nested dictionaries.
63
+ For keys that exist in both dictionaries, the values from dict2 take precedence.
64
+ If a key exists in both dictionaries and the values are dictionaries themselves,
65
+ they are merged recursively.
66
+ This function merges only dictionaries. If key is of different type, e.g. list
67
+ it does not work properly.
68
+ """
69
+ merged = dict1.copy()
70
+ for k, v in dict2.items():
71
+ if k in merged and isinstance(v, dict):
72
+ merged[k] = deep_merge_dicts(merged.get(k, {}), v)
73
+ else:
74
+ merged[k] = v
75
+ return merged
76
+
77
+
78
+ def parse_additional_data(spec: dict[str, Any], file_name: str) -> None:
79
+ """Parse additional data from spec files.
80
+
81
+ Parses:
82
+ * schema URLs
83
+ * base IDs
84
+
85
+ Updates the module-level SCHEMA_URLS and BASE_IDS dictionaries
86
+ that can be imported by other modules.
87
+ """
88
+ base_id = spec["$id"]
89
+ for name, _ in spec["$defs"].items():
90
+ SCHEMA_URLS[name] = f"{base_id}#/$defs/{name}"
91
+ BASE_IDS[file_name] = spec["$id"]
92
+
93
+
94
+ def load_specs(base_spec_location: pathlib.Path, facets_spec_location: pathlib.Path) -> list[pathlib.Path]:
95
+ """Load base `OpenLineage.json` and other facets' spec files"""
96
+ locations = []
97
+ if facets_spec_location.is_dir():
98
+ facets_spec_files = sorted(pathlib.Path(os.path.abspath(facets_spec_location)).glob("*.json"))
99
+ else:
100
+ facets_spec_files = [pathlib.Path(os.path.abspath(facets_spec_location))]
101
+
102
+ file_specs = [
103
+ base_spec_location.resolve(), # Base spec must be the first element
104
+ *facets_spec_files,
105
+ ]
106
+ for file_spec in file_specs:
107
+ spec = json.loads(file_spec.read_text())
108
+ parse_additional_data(spec, file_spec.name)
109
+ locations.append(file_spec)
110
+
111
+ return locations
112
+
113
+
114
+ def parse_and_generate(
115
+ locations: list[pathlib.Path], extra_template_data: dict[str, Any] | None = None
116
+ ) -> dict[str, Any]:
117
+ """Parse and generate data models from a given specification."""
118
+ temporary_locations = []
119
+
120
+ current_dir = pathlib.Path.cwd()
121
+ with tempfile.TemporaryDirectory() as tmp:
122
+ tmp_directory = pathlib.Path(tmp).resolve()
123
+ for location in locations:
124
+ tmp_location = (tmp_directory / location.name).resolve()
125
+ tmp_location.write_text(location.read_text())
126
+ temporary_locations.append(tmp_location)
127
+
128
+ os.chdir(tmp_directory)
129
+ # first parse OpenLineage.json
130
+ parser = JsonSchemaParser(
131
+ source=temporary_locations[:1],
132
+ data_model_type=NEW_MODEL,
133
+ data_model_root_type=DATA_MODELS_TYPES.root_model,
134
+ data_model_field_type=DATA_MODELS_TYPES.field_model,
135
+ data_type_manager_type=DATA_MODELS_TYPES.data_type_manager,
136
+ dump_resolve_reference_action=DATA_MODELS_TYPES.dump_resolve_reference_action,
137
+ special_field_name_prefix="",
138
+ use_schema_description=True,
139
+ field_constraints=True,
140
+ use_union_operator=True,
141
+ use_standard_collections=True,
142
+ base_class="openlineage.client.utils.RedactMixin",
143
+ class_name="ClassToBeSkipped",
144
+ use_field_description=True,
145
+ use_double_quotes=True,
146
+ keep_model_order=True,
147
+ custom_template_dir=TEMPLATES_LOCATION,
148
+ extra_template_data=extra_template_data, # type: ignore[arg-type]
149
+ additional_imports=[
150
+ "typing.ClassVar",
151
+ "typing.Any",
152
+ "typing.cast",
153
+ "openlineage.client.constants.DEFAULT_PRODUCER",
154
+ ],
155
+ )
156
+
157
+ # keep information about uuid and date-time formats
158
+ # this is going to be changed back to str type hint in jinja template
159
+ data_type_manager: pydantic_model.DataTypeManager = parser.data_type_manager # type: ignore[assignment]
160
+ uuid_type = copy.deepcopy(data_type_manager.type_map[Types.uuid])
161
+ uuid_type.type = "uuid"
162
+ data_type_manager.type_map[Types.uuid] = uuid_type
163
+
164
+ date_time_type = copy.deepcopy(data_type_manager.type_map[Types.date_time])
165
+ date_time_type.type = "date-time"
166
+ data_type_manager.type_map[Types.date_time] = date_time_type
167
+
168
+ uri_type = copy.deepcopy(data_type_manager.type_map[Types.date_time])
169
+ uri_type.type = "uri"
170
+ data_type_manager.type_map[Types.uri] = uri_type
171
+
172
+ parser.parse(format_=False)
173
+
174
+ # parse rest of spec
175
+ parser.source = temporary_locations[1:]
176
+
177
+ # change paths so that parser sees base objects as local
178
+ parser.model_resolver.references = {
179
+ k.replace("OpenLineage.json", BASE_IDS["OpenLineage.json"]): v
180
+ for k, v in parser.model_resolver.references.items()
181
+ }
182
+
183
+ output = parser.parse(format_=False)
184
+
185
+ # go back to original directory
186
+ os.chdir(current_dir)
187
+
188
+ return output # type: ignore[return-value]
189
+
190
+
191
+ def separate_imports(code: str) -> tuple[str, str]:
192
+ """Separates a Python script code (as string) into imports and the rest."""
193
+ imports_section = []
194
+ rest_of_code = []
195
+ in_import = False
196
+ for line in code.splitlines():
197
+ if line.startswith(("import ", "from ")):
198
+ in_import = True
199
+ elif in_import and line.strip() == "":
200
+ in_import = False
201
+ if in_import:
202
+ imports_section.append(line)
203
+ else:
204
+ rest_of_code.append(line)
205
+ return ("\n".join(imports_section), "\n".join(rest_of_code))
206
+
207
+
208
+ def format_and_save_output(
209
+ output: str,
210
+ location: pathlib.Path,
211
+ add_set_producer_code: bool | None = False,
212
+ header: str | None = None,
213
+ ) -> None:
214
+ """Adjust and format generated file."""
215
+
216
+ # save temporary file to the same directory that output
217
+ # so that ruff receives same rules
218
+ with tempfile.NamedTemporaryFile(
219
+ "w", prefix=location.stem.lower(), suffix=".py", dir=location.parent, delete=False
220
+ ) as tmpfile:
221
+ if header:
222
+ tmpfile.write(header + "\n")
223
+ output = output.replace("from .OpenLineage", "from openlineage.client.generated.base")
224
+ imports_section, rest_of_code = separate_imports(output)
225
+ tmpfile.write(imports_section)
226
+ if add_set_producer_code:
227
+ tmpfile.write(SET_PRODUCER_CODE)
228
+ tmpfile.write(rest_of_code)
229
+ tmpfile.flush()
230
+
231
+ # run ruff lint
232
+ with subprocess.Popen(
233
+ args=["ruff", "check", tmpfile.name, "--fix"],
234
+ stdout=subprocess.DEVNULL,
235
+ stderr=subprocess.DEVNULL,
236
+ close_fds=True,
237
+ ) as lint_process:
238
+ if lint_process.returncode:
239
+ log.warning("Ruff lint failed: %s", lint_process.returncode)
240
+
241
+ # run ruff format
242
+ with subprocess.Popen(
243
+ args=["ruff", "format", tmpfile.name],
244
+ stdout=subprocess.DEVNULL,
245
+ stderr=subprocess.DEVNULL,
246
+ close_fds=True,
247
+ ) as format_process:
248
+ if format_process.returncode:
249
+ log.warning("Ruff lint failed: %s", format_process.returncode)
250
+
251
+ # move file to output location
252
+ if location.name == "open_lineage.py":
253
+ location = location.with_name("base.py")
254
+ os.rename(tmpfile.name, location)
255
+
256
+ if lint_process.returncode or format_process.returncode:
257
+ log.warning("%s failed on ruff.", location)