data-rentgen 0.1.0__py3-none-any.whl

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 (199) hide show
  1. data_rentgen/__init__.py +12 -0
  2. data_rentgen/consumer/__init__.py +56 -0
  3. data_rentgen/consumer/__main__.py +36 -0
  4. data_rentgen/consumer/extractors/__init__.py +30 -0
  5. data_rentgen/consumer/extractors/batch.py +252 -0
  6. data_rentgen/consumer/extractors/dataset.py +145 -0
  7. data_rentgen/consumer/extractors/input.py +25 -0
  8. data_rentgen/consumer/extractors/job.py +36 -0
  9. data_rentgen/consumer/extractors/operation.py +62 -0
  10. data_rentgen/consumer/extractors/output.py +32 -0
  11. data_rentgen/consumer/extractors/run.py +207 -0
  12. data_rentgen/consumer/extractors/schema.py +15 -0
  13. data_rentgen/consumer/openlineage/__init__.py +9 -0
  14. data_rentgen/consumer/openlineage/base.py +10 -0
  15. data_rentgen/consumer/openlineage/dataset.py +37 -0
  16. data_rentgen/consumer/openlineage/dataset_facets/__init__.py +87 -0
  17. data_rentgen/consumer/openlineage/dataset_facets/base.py +22 -0
  18. data_rentgen/consumer/openlineage/dataset_facets/datasource.py +15 -0
  19. data_rentgen/consumer/openlineage/dataset_facets/documentation.py +14 -0
  20. data_rentgen/consumer/openlineage/dataset_facets/input_statistics.py +18 -0
  21. data_rentgen/consumer/openlineage/dataset_facets/lifecycle_change.py +43 -0
  22. data_rentgen/consumer/openlineage/dataset_facets/output_statistics.py +18 -0
  23. data_rentgen/consumer/openlineage/dataset_facets/schema.py +28 -0
  24. data_rentgen/consumer/openlineage/dataset_facets/storage.py +15 -0
  25. data_rentgen/consumer/openlineage/dataset_facets/symlinks.py +38 -0
  26. data_rentgen/consumer/openlineage/job.py +17 -0
  27. data_rentgen/consumer/openlineage/job_facets/__init__.py +33 -0
  28. data_rentgen/consumer/openlineage/job_facets/base.py +10 -0
  29. data_rentgen/consumer/openlineage/job_facets/documentation.py +12 -0
  30. data_rentgen/consumer/openlineage/job_facets/job_type.py +66 -0
  31. data_rentgen/consumer/openlineage/run.py +17 -0
  32. data_rentgen/consumer/openlineage/run_event.py +45 -0
  33. data_rentgen/consumer/openlineage/run_facets/__init__.py +63 -0
  34. data_rentgen/consumer/openlineage/run_facets/airflow.py +77 -0
  35. data_rentgen/consumer/openlineage/run_facets/base.py +10 -0
  36. data_rentgen/consumer/openlineage/run_facets/parent_run.py +32 -0
  37. data_rentgen/consumer/openlineage/run_facets/processing_engine.py +43 -0
  38. data_rentgen/consumer/openlineage/run_facets/spark_application.py +32 -0
  39. data_rentgen/consumer/openlineage/run_facets/spark_job.py +15 -0
  40. data_rentgen/consumer/settings/__init__.py +54 -0
  41. data_rentgen/consumer/settings/consumer.py +215 -0
  42. data_rentgen/consumer/settings/kafka.py +79 -0
  43. data_rentgen/consumer/settings/security.py +54 -0
  44. data_rentgen/consumer/subscribers.py +97 -0
  45. data_rentgen/db/__init__.py +2 -0
  46. data_rentgen/db/factory.py +30 -0
  47. data_rentgen/db/migrations/README +1 -0
  48. data_rentgen/db/migrations/__main__.py +38 -0
  49. data_rentgen/db/migrations/alembic.ini +47 -0
  50. data_rentgen/db/migrations/env.py +101 -0
  51. data_rentgen/db/migrations/script.py.mako +24 -0
  52. data_rentgen/db/migrations/versions/.keep +0 -0
  53. data_rentgen/db/migrations/versions/2024-06-27_026de1556610_create_job.py +58 -0
  54. data_rentgen/db/migrations/versions/2024-06-27_0b9aac68402b_create_operation.py +43 -0
  55. data_rentgen/db/migrations/versions/2024-06-27_2cb695c0a318_create_dataset.py +61 -0
  56. data_rentgen/db/migrations/versions/2024-06-27_3357c8f914aa_create_write.py +50 -0
  57. data_rentgen/db/migrations/versions/2024-06-27_412d5fbdb362_create_input_output.py +49 -0
  58. data_rentgen/db/migrations/versions/2024-06-27_5f8fff06dd76_create_run.py +58 -0
  59. data_rentgen/db/migrations/versions/2024-06-27_81153f43e276_create_schema.py +34 -0
  60. data_rentgen/db/migrations/versions/2024-06-27_c2c583123133_create_dataset_symlink.py +54 -0
  61. data_rentgen/db/migrations/versions/2024-06-27_c324e48e0f71_create_address.py +60 -0
  62. data_rentgen/db/migrations/versions/2024-06-27_e730f34b9893_create_location.py +53 -0
  63. data_rentgen/db/migrations/versions/2024-06-27_f9a114951102_create_user.py +32 -0
  64. data_rentgen/db/models/__init__.py +37 -0
  65. data_rentgen/db/models/address.py +59 -0
  66. data_rentgen/db/models/base.py +29 -0
  67. data_rentgen/db/models/dataset.py +66 -0
  68. data_rentgen/db/models/dataset_symlink.py +56 -0
  69. data_rentgen/db/models/input.py +117 -0
  70. data_rentgen/db/models/job.py +78 -0
  71. data_rentgen/db/models/location.py +66 -0
  72. data_rentgen/db/models/operation.py +118 -0
  73. data_rentgen/db/models/output.py +142 -0
  74. data_rentgen/db/models/run.py +175 -0
  75. data_rentgen/db/models/schema.py +30 -0
  76. data_rentgen/db/models/user.py +15 -0
  77. data_rentgen/db/repositories/__init__.py +2 -0
  78. data_rentgen/db/repositories/base.py +84 -0
  79. data_rentgen/db/repositories/dataset.py +120 -0
  80. data_rentgen/db/repositories/dataset_symlink.py +60 -0
  81. data_rentgen/db/repositories/input.py +214 -0
  82. data_rentgen/db/repositories/job.py +123 -0
  83. data_rentgen/db/repositories/location.py +148 -0
  84. data_rentgen/db/repositories/operation.py +133 -0
  85. data_rentgen/db/repositories/output.py +221 -0
  86. data_rentgen/db/repositories/run.py +207 -0
  87. data_rentgen/db/repositories/schema.py +43 -0
  88. data_rentgen/db/repositories/user.py +31 -0
  89. data_rentgen/db/scripts/create_partitions.py +140 -0
  90. data_rentgen/db/settings.py +43 -0
  91. data_rentgen/db/utils/fields.py +13 -0
  92. data_rentgen/db/utils/search.py +93 -0
  93. data_rentgen/db/utils/uuid.py +90 -0
  94. data_rentgen/dependencies/__init__.py +6 -0
  95. data_rentgen/dependencies/stub.py +51 -0
  96. data_rentgen/dto/__init__.py +39 -0
  97. data_rentgen/dto/dataset.py +29 -0
  98. data_rentgen/dto/dataset_symlink.py +38 -0
  99. data_rentgen/dto/input.py +50 -0
  100. data_rentgen/dto/job.py +39 -0
  101. data_rentgen/dto/location.py +25 -0
  102. data_rentgen/dto/operation.py +60 -0
  103. data_rentgen/dto/output.py +69 -0
  104. data_rentgen/dto/pagination.py +42 -0
  105. data_rentgen/dto/run.py +77 -0
  106. data_rentgen/dto/schema.py +24 -0
  107. data_rentgen/dto/user.py +22 -0
  108. data_rentgen/exceptions/__init__.py +15 -0
  109. data_rentgen/exceptions/auth.py +34 -0
  110. data_rentgen/exceptions/base.py +24 -0
  111. data_rentgen/exceptions/entity.py +46 -0
  112. data_rentgen/exceptions/redirect.py +21 -0
  113. data_rentgen/logging/__init__.py +2 -0
  114. data_rentgen/logging/exceptions.py +6 -0
  115. data_rentgen/logging/presets/colored.yml +63 -0
  116. data_rentgen/logging/presets/json.yml +62 -0
  117. data_rentgen/logging/presets/plain.yml +63 -0
  118. data_rentgen/logging/settings.py +88 -0
  119. data_rentgen/logging/setup_logging.py +29 -0
  120. data_rentgen/py.typed +0 -0
  121. data_rentgen/server/__init__.py +49 -0
  122. data_rentgen/server/__main__.py +35 -0
  123. data_rentgen/server/api/__init__.py +2 -0
  124. data_rentgen/server/api/handlers.py +138 -0
  125. data_rentgen/server/api/monitoring.py +13 -0
  126. data_rentgen/server/api/router.py +11 -0
  127. data_rentgen/server/api/v1/router/__init__.py +20 -0
  128. data_rentgen/server/api/v1/router/auth.py +53 -0
  129. data_rentgen/server/api/v1/router/dataset.py +54 -0
  130. data_rentgen/server/api/v1/router/job.py +54 -0
  131. data_rentgen/server/api/v1/router/location.py +50 -0
  132. data_rentgen/server/api/v1/router/operation.py +60 -0
  133. data_rentgen/server/api/v1/router/run.py +57 -0
  134. data_rentgen/server/api/v1/router/user.py +27 -0
  135. data_rentgen/server/errors/__init__.py +18 -0
  136. data_rentgen/server/errors/base.py +19 -0
  137. data_rentgen/server/errors/registration.py +65 -0
  138. data_rentgen/server/errors/schemas/__init__.py +15 -0
  139. data_rentgen/server/errors/schemas/invalid_request.py +30 -0
  140. data_rentgen/server/errors/schemas/not_authorized.py +32 -0
  141. data_rentgen/server/errors/schemas/not_found.py +26 -0
  142. data_rentgen/server/middlewares/__init__.py +34 -0
  143. data_rentgen/server/middlewares/application_version.py +50 -0
  144. data_rentgen/server/middlewares/cors.py +18 -0
  145. data_rentgen/server/middlewares/monitoring/__init__.py +7 -0
  146. data_rentgen/server/middlewares/monitoring/metrics.py +44 -0
  147. data_rentgen/server/middlewares/openapi.py +113 -0
  148. data_rentgen/server/middlewares/request_id.py +21 -0
  149. data_rentgen/server/middlewares/session.py +16 -0
  150. data_rentgen/server/middlewares/static_files.py +17 -0
  151. data_rentgen/server/providers/__init__.py +2 -0
  152. data_rentgen/server/providers/auth/__init__.py +11 -0
  153. data_rentgen/server/providers/auth/base_provider.py +105 -0
  154. data_rentgen/server/providers/auth/dummy_provider.py +99 -0
  155. data_rentgen/server/providers/auth/keycloak_provider.py +115 -0
  156. data_rentgen/server/schemas/__init__.py +8 -0
  157. data_rentgen/server/schemas/ping.py +12 -0
  158. data_rentgen/server/schemas/v1/__init__.py +69 -0
  159. data_rentgen/server/schemas/v1/address.py +9 -0
  160. data_rentgen/server/schemas/v1/auth.py +13 -0
  161. data_rentgen/server/schemas/v1/dataset.py +36 -0
  162. data_rentgen/server/schemas/v1/job.py +36 -0
  163. data_rentgen/server/schemas/v1/lineage.py +178 -0
  164. data_rentgen/server/schemas/v1/location.py +39 -0
  165. data_rentgen/server/schemas/v1/operation.py +113 -0
  166. data_rentgen/server/schemas/v1/pagination.py +54 -0
  167. data_rentgen/server/schemas/v1/run.py +143 -0
  168. data_rentgen/server/schemas/v1/user.py +8 -0
  169. data_rentgen/server/scripts/export_openapi_schema.py +22 -0
  170. data_rentgen/server/services/__init__.py +6 -0
  171. data_rentgen/server/services/get_user.py +30 -0
  172. data_rentgen/server/services/lineage.py +859 -0
  173. data_rentgen/server/settings/__init__.py +55 -0
  174. data_rentgen/server/settings/application_version.py +23 -0
  175. data_rentgen/server/settings/auth/__init__.py +29 -0
  176. data_rentgen/server/settings/auth/dummy.py +20 -0
  177. data_rentgen/server/settings/auth/jwt.py +43 -0
  178. data_rentgen/server/settings/auth/keycloak.py +37 -0
  179. data_rentgen/server/settings/cors.py +59 -0
  180. data_rentgen/server/settings/monitoring.py +67 -0
  181. data_rentgen/server/settings/openapi.py +149 -0
  182. data_rentgen/server/settings/request_id.py +37 -0
  183. data_rentgen/server/settings/server.py +70 -0
  184. data_rentgen/server/settings/session.py +59 -0
  185. data_rentgen/server/settings/static_files.py +35 -0
  186. data_rentgen/server/static/.gitkeep +0 -0
  187. data_rentgen/server/utils/__init__.py +2 -0
  188. data_rentgen/server/utils/jwt.py +25 -0
  189. data_rentgen/server/utils/lineage_response.py +116 -0
  190. data_rentgen/server/utils/slug.py +10 -0
  191. data_rentgen/services/__init__.py +6 -0
  192. data_rentgen/services/uow.py +45 -0
  193. data_rentgen/utils/__init__.py +5 -0
  194. data_rentgen/utils/uuid.py +25 -0
  195. data_rentgen-0.1.0.dist-info/LICENSE.txt +203 -0
  196. data_rentgen-0.1.0.dist-info/METADATA +121 -0
  197. data_rentgen-0.1.0.dist-info/RECORD +199 -0
  198. data_rentgen-0.1.0.dist-info/WHEEL +4 -0
  199. data_rentgen-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,12 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # _raw_version could contain pre-release version, like 0.0.1dev123
5
+ # value is updated automatically by `poetry version ...` and poetry-bumpversion plugin
6
+ _raw_version = "0.0.1"
7
+
8
+ # version always contain only release number like 0.0.1
9
+ __version__ = ".".join(_raw_version.split(".")[:3])
10
+
11
+ # version tuple always contains only integer parts, like (0, 0, 1)
12
+ __version_tuple__ = tuple(map(int, __version__.split(".")))
@@ -0,0 +1,56 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+
6
+ from fast_depends import dependency_provider
7
+ from faststream import FastStream
8
+ from faststream.kafka import KafkaBroker
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ import data_rentgen
12
+ from data_rentgen.consumer.settings import ConsumerApplicationSettings
13
+ from data_rentgen.consumer.settings.security import get_broker_security
14
+ from data_rentgen.consumer.subscribers import runs_events_subscriber
15
+ from data_rentgen.db.factory import create_session_factory
16
+ from data_rentgen.logging.setup_logging import setup_logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def broker_factory(settings: ConsumerApplicationSettings) -> KafkaBroker:
22
+ broker = KafkaBroker(
23
+ bootstrap_servers=settings.kafka.bootstrap_servers,
24
+ security=get_broker_security(settings.kafka.security),
25
+ compression_type=settings.kafka.compression.value if settings.kafka.compression else None,
26
+ client_id=f"data-rentgen-{data_rentgen.__version__}",
27
+ logger=logger,
28
+ )
29
+
30
+ # register subscribers using settings
31
+ consumer_settings = settings.consumer.model_dump(exclude={"topics_list", "topics_pattern"})
32
+ broker.subscriber(
33
+ *settings.consumer.topics_list,
34
+ pattern=settings.consumer.topics_pattern,
35
+ **consumer_settings,
36
+ batch=True,
37
+ )(runs_events_subscriber)
38
+
39
+ dependency_provider.override(AsyncSession, create_session_factory(settings.database))
40
+ return broker
41
+
42
+
43
+ def application_factory(settings: ConsumerApplicationSettings) -> FastStream:
44
+ return FastStream(
45
+ broker=broker_factory(settings),
46
+ title="Data.Rentgen",
47
+ description="Data.Rentgen is a nextgen DataLineage service",
48
+ version=data_rentgen.__version__,
49
+ logger=logger,
50
+ )
51
+
52
+
53
+ def get_application():
54
+ settings = ConsumerApplicationSettings()
55
+ setup_logging(settings.logging)
56
+ return application_factory(settings=settings)
@@ -0,0 +1,36 @@
1
+ #!/bin/env python3
2
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from faststream.cli.main import cli
12
+
13
+ here = Path(__file__).resolve()
14
+
15
+
16
+ def main(prog_name: str | None = None, args: list[str] | None = None):
17
+ """Run FastStream and pass the command line arguments to it."""
18
+ if args is None:
19
+ args = sys.argv.copy()
20
+ prog_name = args.pop(0)
21
+
22
+ if not prog_name:
23
+ prog_name = os.fspath(here)
24
+
25
+ args = args.copy()
26
+ # prepend config path before command line arguments
27
+ args.insert(0, "run")
28
+ args.insert(1, "data_rentgen.consumer:get_application")
29
+ args.insert(2, "--factory")
30
+
31
+ # call uvicorn
32
+ cli(args, prog_name=prog_name)
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,30 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from data_rentgen.consumer.extractors.batch import BatchExtractionResult, extract_batch
5
+ from data_rentgen.consumer.extractors.dataset import (
6
+ connect_dataset_with_symlinks,
7
+ extract_dataset,
8
+ extract_dataset_and_symlinks,
9
+ )
10
+ from data_rentgen.consumer.extractors.input import extract_input
11
+ from data_rentgen.consumer.extractors.job import extract_job
12
+ from data_rentgen.consumer.extractors.operation import extract_operation
13
+ from data_rentgen.consumer.extractors.output import extract_output
14
+ from data_rentgen.consumer.extractors.run import extract_run, extract_run_minimal
15
+ from data_rentgen.consumer.extractors.schema import extract_schema
16
+
17
+ __all__ = [
18
+ "extract_dataset_and_symlinks",
19
+ "extract_dataset",
20
+ "connect_dataset_with_symlinks",
21
+ "extract_job",
22
+ "extract_run",
23
+ "extract_run_minimal",
24
+ "extract_operation",
25
+ "extract_input",
26
+ "extract_output",
27
+ "extract_schema",
28
+ "extract_batch",
29
+ "BatchExtractionResult",
30
+ ]
@@ -0,0 +1,252 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ from __future__ import annotations
4
+
5
+ from typing import TypeVar
6
+
7
+ from data_rentgen.consumer.extractors.input import extract_input
8
+ from data_rentgen.consumer.extractors.operation import extract_operation
9
+ from data_rentgen.consumer.extractors.output import extract_output
10
+ from data_rentgen.consumer.extractors.run import extract_run
11
+ from data_rentgen.consumer.openlineage.job_facets.job_type import OpenLineageJobType
12
+ from data_rentgen.consumer.openlineage.run_event import OpenLineageRunEvent
13
+ from data_rentgen.dto import (
14
+ DatasetDTO,
15
+ InputDTO,
16
+ JobDTO,
17
+ LocationDTO,
18
+ OperationDTO,
19
+ OutputDTO,
20
+ RunDTO,
21
+ SchemaDTO,
22
+ UserDTO,
23
+ )
24
+ from data_rentgen.dto.dataset_symlink import DatasetSymlinkDTO
25
+
26
+ T = TypeVar(
27
+ "T",
28
+ LocationDTO,
29
+ DatasetDTO,
30
+ DatasetSymlinkDTO,
31
+ JobDTO,
32
+ RunDTO,
33
+ OperationDTO,
34
+ InputDTO,
35
+ OutputDTO,
36
+ SchemaDTO,
37
+ UserDTO,
38
+ )
39
+
40
+
41
+ class BatchExtractionResult: # noqa: WPS338, WPS214
42
+ """Track results of batch extraction.
43
+
44
+ Calling any ``add_*`` method will add DTO item to the result, including nested DTOs,
45
+ like ``OperationDTO`` -> ``RunDTO`` -> ``JobDTO`` -> ``LocationDTO``, and so on.
46
+
47
+ Each DTO type is tracked separately. DTOs with same ``unique_key`` are merged into one final DTO,
48
+ by calling ``existing.merge(new)``. The resulting final DTO contains all non-null attributes of original DTOs.
49
+ Last DTO in the chain has a higher priority than previuos ones.
50
+ For example ``RunDTO(status=STARTED, started_at=...).merge(RunDTO(status=SUCCEEDED, ended_at=...))``
51
+ produces final ``RunDTO(status=SUCCEEDED, started_at=..., ended_at=...)``.
52
+
53
+ Calling get methods, like ``jobs()``, will return the list of tracked DTOs with resolved
54
+ cross-links. For example, iterating over ``jobs()`` with return the same objects
55
+ as in ``[run.job for run in runs()]``.
56
+ This makes modification of nested DTOs easier, all changes will be reflected in the parent DTOs as well.
57
+ """
58
+
59
+ def __init__(self):
60
+ self._locations: dict[tuple, LocationDTO] = {}
61
+ self._datasets: dict[tuple, DatasetDTO] = {}
62
+ self._dataset_symlinks: dict[tuple, DatasetSymlinkDTO] = {}
63
+ self._jobs: dict[tuple, JobDTO] = {}
64
+ self._runs: dict[tuple, RunDTO] = {}
65
+ self._operations: dict[tuple, OperationDTO] = {}
66
+ self._inputs: dict[tuple, InputDTO] = {}
67
+ self._outputs: dict[tuple, OutputDTO] = {}
68
+ self._schemas: dict[tuple, SchemaDTO] = {}
69
+ self._users: dict[tuple, UserDTO] = {}
70
+
71
+ def __repr__(self):
72
+ return (
73
+ "ExtractionResult(" # noqa: WPS237
74
+ f"locations={len(self._locations)}, "
75
+ f"datasets={len(self._datasets)}, "
76
+ f"dataset_symlinks={len(self._dataset_symlinks)}, "
77
+ f"jobs={len(self._jobs)}, "
78
+ f"runs={len(self._runs)}, "
79
+ f"operations={len(self._operations)}, "
80
+ f"inputs={len(self._inputs)}, "
81
+ f"outputs={len(self._outputs)}, "
82
+ f"schemas={len(self._schemas)}, "
83
+ f"users={len(self._users)}"
84
+ ")"
85
+ )
86
+
87
+ @staticmethod
88
+ def _add(context: dict[tuple, T], new_item: T) -> dict[tuple, T]: # noqa: WPS602
89
+ if new_item.unique_key in context:
90
+ context[new_item.unique_key] = context[new_item.unique_key].merge(new_item)
91
+ else:
92
+ context[new_item.unique_key] = new_item
93
+ return context
94
+
95
+ def add_location(self, location: LocationDTO):
96
+ self._add(self._locations, location)
97
+
98
+ def add_dataset(self, dataset: DatasetDTO):
99
+ self._add(self._datasets, dataset)
100
+ self.add_location(dataset.location)
101
+
102
+ def add_dataset_symlink(self, dataset_symlink: DatasetSymlinkDTO):
103
+ self._add(self._dataset_symlinks, dataset_symlink)
104
+ self.add_dataset(dataset_symlink.from_dataset)
105
+ self.add_dataset(dataset_symlink.to_dataset)
106
+
107
+ def add_job(self, job: JobDTO):
108
+ self._add(self._jobs, job)
109
+ self.add_location(job.location)
110
+
111
+ def add_run(self, run: RunDTO):
112
+ self._add(self._runs, run)
113
+ self.add_job(run.job)
114
+ if run.parent_run:
115
+ self.add_run(run.parent_run)
116
+ if run.user:
117
+ self.add_user(run.user)
118
+
119
+ def add_operation(self, operation: OperationDTO):
120
+ self._add(self._operations, operation)
121
+ self.add_run(operation.run)
122
+
123
+ def add_input(self, input: InputDTO):
124
+ self._add(self._inputs, input)
125
+ self.add_operation(input.operation)
126
+ self.add_dataset(input.dataset)
127
+ if input.schema:
128
+ self.add_schema(input.schema)
129
+
130
+ def add_output(self, output: OutputDTO):
131
+ self._add(self._outputs, output)
132
+ self.add_operation(output.operation)
133
+ self.add_dataset(output.dataset)
134
+ if output.schema:
135
+ self.add_schema(output.schema)
136
+
137
+ def add_schema(self, schema: SchemaDTO):
138
+ self._add(self._schemas, schema)
139
+
140
+ def add_user(self, user: UserDTO):
141
+ self._add(self._users, user)
142
+
143
+ def _get_location(self, location_key: tuple) -> LocationDTO:
144
+ return self._locations[location_key]
145
+
146
+ def _get_schema(self, schema_key: tuple) -> SchemaDTO:
147
+ return self._schemas[schema_key]
148
+
149
+ def _get_user(self, user_key: tuple) -> UserDTO:
150
+ return self._users[user_key]
151
+
152
+ def _get_dataset(self, dataset_key: tuple) -> DatasetDTO:
153
+ dataset = self._datasets[dataset_key]
154
+ dataset.location = self._get_location(dataset.location.unique_key)
155
+ return dataset
156
+
157
+ def _get_dataset_symlink(self, dataset_symlink_key: tuple) -> DatasetSymlinkDTO:
158
+ dataset_symlink = self._dataset_symlinks[dataset_symlink_key]
159
+ dataset_symlink.from_dataset = self._get_dataset(dataset_symlink.from_dataset.unique_key)
160
+ dataset_symlink.to_dataset = self._get_dataset(dataset_symlink.to_dataset.unique_key)
161
+ return dataset_symlink
162
+
163
+ def _get_job(self, job_key: tuple) -> JobDTO:
164
+ job = self._jobs[job_key]
165
+ job.location = self._get_location(job.location.unique_key)
166
+ return job
167
+
168
+ def _get_run(self, run_key: tuple) -> RunDTO:
169
+ run = self._runs[run_key]
170
+ run.job = self._get_job(run.job.unique_key)
171
+ if run.parent_run:
172
+ run.parent_run = self._get_run(run.parent_run.unique_key)
173
+ if run.user:
174
+ run.user = self._get_user(run.user.unique_key)
175
+ return run
176
+
177
+ def _get_operation(self, operation_key: tuple) -> OperationDTO:
178
+ operation = self._operations[operation_key]
179
+ operation.run = self._get_run(operation.run.unique_key)
180
+ return operation
181
+
182
+ def _get_input(self, input_key: tuple) -> InputDTO:
183
+ input = self._inputs[input_key]
184
+ input.operation = self._get_operation(input.operation.unique_key)
185
+ input.dataset = self._get_dataset(input.dataset.unique_key)
186
+ if input.schema:
187
+ input.schema = self._get_schema(input.schema.unique_key)
188
+ return input
189
+
190
+ def _get_output(self, output_key: tuple) -> OutputDTO:
191
+ output = self._outputs[output_key]
192
+ output.operation = self._get_operation(output.operation.unique_key)
193
+ output.dataset = self._get_dataset(output.dataset.unique_key)
194
+ if output.schema:
195
+ output.schema = self._get_schema(output.schema.unique_key)
196
+ return output
197
+
198
+ def locations(self) -> list[LocationDTO]:
199
+ return list(map(self._get_location, self._locations))
200
+
201
+ def datasets(self) -> list[DatasetDTO]:
202
+ return list(map(self._get_dataset, self._datasets))
203
+
204
+ def dataset_symlinks(self) -> list[DatasetSymlinkDTO]:
205
+ return list(map(self._get_dataset_symlink, self._dataset_symlinks))
206
+
207
+ def jobs(self) -> list[JobDTO]:
208
+ return list(map(self._get_job, self._jobs))
209
+
210
+ def runs(self) -> list[RunDTO]:
211
+ return list(map(self._get_run, self._runs))
212
+
213
+ def operations(self) -> list[OperationDTO]:
214
+ return list(map(self._get_operation, self._operations))
215
+
216
+ def inputs(self) -> list[InputDTO]:
217
+ return list(map(self._get_input, self._inputs))
218
+
219
+ def outputs(self) -> list[OutputDTO]:
220
+ return list(map(self._get_output, self._outputs))
221
+
222
+ def schemas(self) -> list[SchemaDTO]:
223
+ return list(map(self._get_schema, self._schemas))
224
+
225
+ def users(self) -> list[UserDTO]:
226
+ return list(map(self._get_user, self._users))
227
+
228
+
229
+ def extract_batch(events: list[OpenLineageRunEvent]) -> BatchExtractionResult:
230
+ result = BatchExtractionResult()
231
+
232
+ for event in events:
233
+ if event.job.facets.jobType and event.job.facets.jobType.jobType == OpenLineageJobType.JOB:
234
+ operation = extract_operation(event)
235
+ result.add_operation(operation)
236
+ for input_dataset in event.inputs:
237
+ input, symlinks = extract_input(operation, input_dataset)
238
+ result.add_input(input)
239
+ for symlink in symlinks:
240
+ result.add_dataset_symlink(symlink)
241
+
242
+ for output_dataset in event.outputs:
243
+ output, symlinks = extract_output(operation, output_dataset)
244
+ result.add_output(output)
245
+ for symlink in symlinks: # noqa: WPS440
246
+ result.add_dataset_symlink(symlink)
247
+
248
+ else:
249
+ run = extract_run(event)
250
+ result.add_run(run)
251
+
252
+ return result
@@ -0,0 +1,145 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ from urllib.parse import urlparse
6
+
7
+ from data_rentgen.consumer.openlineage.dataset import OpenLineageDataset
8
+ from data_rentgen.consumer.openlineage.dataset_facets import (
9
+ OpenLineageStorageDatasetFacet,
10
+ OpenLineageSymlinkIdentifier,
11
+ OpenLineageSymlinkType,
12
+ )
13
+ from data_rentgen.dto import (
14
+ DatasetDTO,
15
+ DatasetSymlinkDTO,
16
+ DatasetSymlinkTypeDTO,
17
+ LocationDTO,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ METASTORE = DatasetSymlinkTypeDTO.METASTORE
23
+ WAREHOUSE = DatasetSymlinkTypeDTO.WAREHOUSE
24
+
25
+
26
+ def connect_dataset_with_symlinks(
27
+ dataset: DatasetDTO,
28
+ symlink: DatasetDTO,
29
+ type: OpenLineageSymlinkType,
30
+ ) -> list[DatasetSymlinkDTO]:
31
+ result = []
32
+ is_metastore_symlink = type == OpenLineageSymlinkType.TABLE
33
+
34
+ result.append(
35
+ DatasetSymlinkDTO(
36
+ from_dataset=dataset,
37
+ to_dataset=symlink,
38
+ type=METASTORE if is_metastore_symlink else WAREHOUSE,
39
+ ),
40
+ )
41
+ result.append(
42
+ DatasetSymlinkDTO(
43
+ from_dataset=symlink,
44
+ to_dataset=dataset,
45
+ type=WAREHOUSE if is_metastore_symlink else METASTORE,
46
+ ),
47
+ )
48
+
49
+ return sorted(result, key=lambda x: x.type)
50
+
51
+
52
+ def extract_dataset(dataset: OpenLineageDataset | OpenLineageSymlinkIdentifier) -> DatasetDTO:
53
+ return DatasetDTO(
54
+ name=dataset.name,
55
+ location=extract_dataset_location(dataset),
56
+ format=extract_dataset_format(dataset),
57
+ )
58
+
59
+
60
+ def extract_dataset_and_symlinks(dataset: OpenLineageDataset) -> tuple[DatasetDTO, list[DatasetSymlinkDTO]]:
61
+ if not dataset.facets.symlinks:
62
+ return extract_dataset(dataset), []
63
+
64
+ table_symlinks = [
65
+ identifier
66
+ for identifier in dataset.facets.symlinks.identifiers
67
+ if identifier.type == OpenLineageSymlinkType.TABLE
68
+ ]
69
+ if table_symlinks:
70
+ # We are swapping the dataset with its TABLE symlink to create a cleaner lineage.
71
+ # For example, by replacing an HDFS file with its corresponding Hive table.
72
+ # This ensures that all operations interact with a single table instead of multiple files (which may represent different partitions).
73
+ # Discussion on this issue: https://github.com/OpenLineage/OpenLineage/issues/2718
74
+
75
+ # TODO: add support for multiple TABLE symlinks
76
+ if len(table_symlinks) > 1:
77
+ logger.warning(
78
+ "Dataset has more than one TABLE symlink. Only the first one will be used for replacement. Symlink name: %s",
79
+ table_symlinks[0].name,
80
+ )
81
+ table_dataset = table_symlinks[0]
82
+ return (
83
+ DatasetDTO(
84
+ name=table_dataset.name,
85
+ location=extract_dataset_location(table_dataset),
86
+ format=extract_dataset_format(table_dataset),
87
+ ),
88
+ connect_dataset_with_symlinks(
89
+ extract_dataset(dataset),
90
+ extract_dataset(table_dataset),
91
+ OpenLineageSymlinkType.TABLE,
92
+ ),
93
+ )
94
+
95
+ symlinks = []
96
+ for identifier in dataset.facets.symlinks.identifiers:
97
+ symlinks.extend(
98
+ connect_dataset_with_symlinks(
99
+ extract_dataset(dataset),
100
+ extract_dataset(identifier),
101
+ identifier.type,
102
+ ),
103
+ )
104
+
105
+ return (
106
+ DatasetDTO(
107
+ name=dataset.name,
108
+ location=extract_dataset_location(dataset),
109
+ format=extract_dataset_format(dataset),
110
+ ),
111
+ symlinks,
112
+ )
113
+
114
+
115
+ def extract_dataset_location(dataset: OpenLineageDataset | OpenLineageSymlinkIdentifier) -> LocationDTO:
116
+ namespace = dataset.namespace
117
+ if namespace == "file":
118
+ # TODO: remove after https://github.com/OpenLineage/OpenLineage/issues/2709
119
+ namespace = "file://"
120
+
121
+ url = urlparse(namespace)
122
+ scheme = url.scheme or "unknown"
123
+
124
+ # TODO: handle S3 bucket properly after https://github.com/OpenLineage/OpenLineage/issues/2816
125
+ netloc = url.netloc or url.path
126
+ hosts = list(filter(None, netloc.split(","))) or ["unknown"]
127
+ return LocationDTO(
128
+ type=scheme,
129
+ name=hosts[0],
130
+ addresses={f"{scheme}://{host}" for host in hosts},
131
+ )
132
+
133
+
134
+ def extract_dataset_format(dataset: OpenLineageDataset | OpenLineageSymlinkIdentifier) -> str | None:
135
+ if isinstance(dataset, OpenLineageSymlinkIdentifier):
136
+ return None
137
+
138
+ match dataset.facets.storage:
139
+ case OpenLineageStorageDatasetFacet(storageLayer="default", fileFormat=file_format):
140
+ # See https://github.com/OpenLineage/OpenLineage/issues/2770
141
+ return file_format
142
+ case OpenLineageStorageDatasetFacet(storageLayer=storage_layer):
143
+ return storage_layer
144
+ case _:
145
+ return None
@@ -0,0 +1,25 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from data_rentgen.consumer.extractors.dataset import extract_dataset_and_symlinks
5
+ from data_rentgen.consumer.extractors.schema import extract_schema
6
+ from data_rentgen.consumer.openlineage.dataset import OpenLineageInputDataset
7
+ from data_rentgen.dto import DatasetSymlinkDTO, InputDTO
8
+ from data_rentgen.dto.operation import OperationDTO
9
+
10
+
11
+ def extract_input(
12
+ operation: OperationDTO,
13
+ dataset: OpenLineageInputDataset,
14
+ ) -> tuple[InputDTO, list[DatasetSymlinkDTO]]:
15
+ dataset_dto, symlinks = extract_dataset_and_symlinks(dataset)
16
+ result = InputDTO(
17
+ operation=operation,
18
+ dataset=dataset_dto,
19
+ schema=extract_schema(dataset),
20
+ )
21
+ if dataset.inputFacets.inputStatistics:
22
+ result.num_rows = dataset.inputFacets.inputStatistics.rows
23
+ result.num_bytes = dataset.inputFacets.inputStatistics.bytes
24
+ result.num_files = dataset.inputFacets.inputStatistics.files
25
+ return result, symlinks
@@ -0,0 +1,36 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from urllib.parse import urlparse
5
+
6
+ from data_rentgen.consumer.openlineage.job import OpenLineageJob
7
+ from data_rentgen.consumer.openlineage.run_facets import OpenLineageParentJob
8
+ from data_rentgen.dto import JobDTO, JobTypeDTO, LocationDTO
9
+
10
+
11
+ def extract_job(job: OpenLineageJob | OpenLineageParentJob) -> JobDTO:
12
+ return JobDTO(
13
+ name=job.name,
14
+ location=extract_job_location(job),
15
+ type=extract_job_type(job),
16
+ )
17
+
18
+
19
+ def extract_job_location(job: OpenLineageJob | OpenLineageParentJob) -> LocationDTO:
20
+ url = urlparse(job.namespace)
21
+ scheme = url.scheme or "unknown"
22
+ netloc = url.netloc or url.path
23
+ return LocationDTO(
24
+ type=scheme,
25
+ name=netloc,
26
+ addresses={f"{scheme}://{netloc}"},
27
+ )
28
+
29
+
30
+ def extract_job_type(job: OpenLineageJob | OpenLineageParentJob) -> JobTypeDTO | None:
31
+ if isinstance(job, OpenLineageJob) and job.facets.jobType:
32
+ job_type = job.facets.jobType.jobType
33
+ integration_type = job.facets.jobType.integration
34
+ return JobTypeDTO(f"{integration_type}_{job_type}")
35
+
36
+ return None
@@ -0,0 +1,62 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from data_rentgen.consumer.extractors.run import extract_run_minimal
5
+ from data_rentgen.consumer.openlineage.run_event import (
6
+ OpenLineageRunEvent,
7
+ OpenLineageRunEventType,
8
+ )
9
+ from data_rentgen.dto import OperationDTO, OperationStatusDTO, OperationTypeDTO
10
+
11
+
12
+ def extract_operation(event: OpenLineageRunEvent) -> OperationDTO:
13
+ # operation always has parent
14
+ run = extract_run_minimal(event.run.facets.parent) # type: ignore[arg-type]
15
+
16
+ # in some cases, operation name may contain raw SELECT query with newlines
17
+ operation_name = " ".join(line.strip() for line in event.job.name.splitlines()).strip()
18
+ # remove parent job name from operation name
19
+ if operation_name.startswith(run.job.name):
20
+ prefix = len(run.job.name) + 1
21
+ operation_name = operation_name[prefix:]
22
+
23
+ operation = OperationDTO(
24
+ id=event.run.runId, # type: ignore [arg-type]
25
+ run=run,
26
+ name=operation_name,
27
+ type=OperationTypeDTO(event.job.facets.jobType.processingType) if event.job.facets.jobType else None,
28
+ )
29
+ enrich_operation_status(operation, event)
30
+ enrich_operation_description(operation, event)
31
+ return operation
32
+
33
+
34
+ def enrich_operation_status(operation: OperationDTO, event: OpenLineageRunEvent) -> OperationDTO:
35
+ match event.eventType:
36
+ case OpenLineageRunEventType.START:
37
+ operation.started_at = event.eventTime
38
+ operation.status = OperationStatusDTO.STARTED
39
+ case OpenLineageRunEventType.RUNNING:
40
+ operation.status = OperationStatusDTO.STARTED
41
+ case OpenLineageRunEventType.COMPLETE:
42
+ operation.ended_at = event.eventTime
43
+ operation.status = OperationStatusDTO.SUCCEEDED
44
+ case OpenLineageRunEventType.FAIL:
45
+ operation.ended_at = event.eventTime
46
+ operation.status = OperationStatusDTO.FAILED
47
+ case OpenLineageRunEventType.ABORT:
48
+ operation.ended_at = event.eventTime
49
+ operation.status = OperationStatusDTO.KILLED
50
+ case OpenLineageRunEventType.OTHER:
51
+ # OTHER is used only to update run statistics
52
+ pass
53
+ return operation
54
+
55
+
56
+ def enrich_operation_description(operation: OperationDTO, event: OpenLineageRunEvent) -> OperationDTO:
57
+ spark_job_details = event.run.facets.spark_jobDetails
58
+ if spark_job_details:
59
+ operation.position = spark_job_details.jobId
60
+ operation.group = spark_job_details.jobGroup
61
+ operation.description = spark_job_details.jobDescription
62
+ return operation
@@ -0,0 +1,32 @@
1
+ # SPDX-FileCopyrightText: 2024 MTS PJSC
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from data_rentgen.consumer.extractors.dataset import extract_dataset_and_symlinks
5
+ from data_rentgen.consumer.extractors.schema import extract_schema
6
+ from data_rentgen.consumer.openlineage.dataset import OpenLineageOutputDataset
7
+ from data_rentgen.dto import DatasetSymlinkDTO, OutputDTO, OutputTypeDTO
8
+ from data_rentgen.dto.operation import OperationDTO
9
+
10
+
11
+ def extract_output(
12
+ operation: OperationDTO,
13
+ dataset: OpenLineageOutputDataset,
14
+ ) -> tuple[OutputDTO, list[DatasetSymlinkDTO]]:
15
+ lifecycle_change = dataset.facets.lifecycleStateChange
16
+ if lifecycle_change:
17
+ output_type = OutputTypeDTO(lifecycle_change.lifecycleStateChange)
18
+ else:
19
+ output_type = OutputTypeDTO.APPEND
20
+ dataset_dto, symlinks = extract_dataset_and_symlinks(dataset)
21
+ result = OutputDTO(
22
+ type=output_type,
23
+ operation=operation,
24
+ dataset=dataset_dto,
25
+ schema=extract_schema(dataset),
26
+ )
27
+ if dataset.outputFacets.outputStatistics:
28
+ result.num_rows = dataset.outputFacets.outputStatistics.rows
29
+ result.num_bytes = dataset.outputFacets.outputStatistics.bytes
30
+ result.num_files = dataset.outputFacets.outputStatistics.files
31
+
32
+ return result, symlinks