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.
- data_rentgen/__init__.py +12 -0
- data_rentgen/consumer/__init__.py +56 -0
- data_rentgen/consumer/__main__.py +36 -0
- data_rentgen/consumer/extractors/__init__.py +30 -0
- data_rentgen/consumer/extractors/batch.py +252 -0
- data_rentgen/consumer/extractors/dataset.py +145 -0
- data_rentgen/consumer/extractors/input.py +25 -0
- data_rentgen/consumer/extractors/job.py +36 -0
- data_rentgen/consumer/extractors/operation.py +62 -0
- data_rentgen/consumer/extractors/output.py +32 -0
- data_rentgen/consumer/extractors/run.py +207 -0
- data_rentgen/consumer/extractors/schema.py +15 -0
- data_rentgen/consumer/openlineage/__init__.py +9 -0
- data_rentgen/consumer/openlineage/base.py +10 -0
- data_rentgen/consumer/openlineage/dataset.py +37 -0
- data_rentgen/consumer/openlineage/dataset_facets/__init__.py +87 -0
- data_rentgen/consumer/openlineage/dataset_facets/base.py +22 -0
- data_rentgen/consumer/openlineage/dataset_facets/datasource.py +15 -0
- data_rentgen/consumer/openlineage/dataset_facets/documentation.py +14 -0
- data_rentgen/consumer/openlineage/dataset_facets/input_statistics.py +18 -0
- data_rentgen/consumer/openlineage/dataset_facets/lifecycle_change.py +43 -0
- data_rentgen/consumer/openlineage/dataset_facets/output_statistics.py +18 -0
- data_rentgen/consumer/openlineage/dataset_facets/schema.py +28 -0
- data_rentgen/consumer/openlineage/dataset_facets/storage.py +15 -0
- data_rentgen/consumer/openlineage/dataset_facets/symlinks.py +38 -0
- data_rentgen/consumer/openlineage/job.py +17 -0
- data_rentgen/consumer/openlineage/job_facets/__init__.py +33 -0
- data_rentgen/consumer/openlineage/job_facets/base.py +10 -0
- data_rentgen/consumer/openlineage/job_facets/documentation.py +12 -0
- data_rentgen/consumer/openlineage/job_facets/job_type.py +66 -0
- data_rentgen/consumer/openlineage/run.py +17 -0
- data_rentgen/consumer/openlineage/run_event.py +45 -0
- data_rentgen/consumer/openlineage/run_facets/__init__.py +63 -0
- data_rentgen/consumer/openlineage/run_facets/airflow.py +77 -0
- data_rentgen/consumer/openlineage/run_facets/base.py +10 -0
- data_rentgen/consumer/openlineage/run_facets/parent_run.py +32 -0
- data_rentgen/consumer/openlineage/run_facets/processing_engine.py +43 -0
- data_rentgen/consumer/openlineage/run_facets/spark_application.py +32 -0
- data_rentgen/consumer/openlineage/run_facets/spark_job.py +15 -0
- data_rentgen/consumer/settings/__init__.py +54 -0
- data_rentgen/consumer/settings/consumer.py +215 -0
- data_rentgen/consumer/settings/kafka.py +79 -0
- data_rentgen/consumer/settings/security.py +54 -0
- data_rentgen/consumer/subscribers.py +97 -0
- data_rentgen/db/__init__.py +2 -0
- data_rentgen/db/factory.py +30 -0
- data_rentgen/db/migrations/README +1 -0
- data_rentgen/db/migrations/__main__.py +38 -0
- data_rentgen/db/migrations/alembic.ini +47 -0
- data_rentgen/db/migrations/env.py +101 -0
- data_rentgen/db/migrations/script.py.mako +24 -0
- data_rentgen/db/migrations/versions/.keep +0 -0
- data_rentgen/db/migrations/versions/2024-06-27_026de1556610_create_job.py +58 -0
- data_rentgen/db/migrations/versions/2024-06-27_0b9aac68402b_create_operation.py +43 -0
- data_rentgen/db/migrations/versions/2024-06-27_2cb695c0a318_create_dataset.py +61 -0
- data_rentgen/db/migrations/versions/2024-06-27_3357c8f914aa_create_write.py +50 -0
- data_rentgen/db/migrations/versions/2024-06-27_412d5fbdb362_create_input_output.py +49 -0
- data_rentgen/db/migrations/versions/2024-06-27_5f8fff06dd76_create_run.py +58 -0
- data_rentgen/db/migrations/versions/2024-06-27_81153f43e276_create_schema.py +34 -0
- data_rentgen/db/migrations/versions/2024-06-27_c2c583123133_create_dataset_symlink.py +54 -0
- data_rentgen/db/migrations/versions/2024-06-27_c324e48e0f71_create_address.py +60 -0
- data_rentgen/db/migrations/versions/2024-06-27_e730f34b9893_create_location.py +53 -0
- data_rentgen/db/migrations/versions/2024-06-27_f9a114951102_create_user.py +32 -0
- data_rentgen/db/models/__init__.py +37 -0
- data_rentgen/db/models/address.py +59 -0
- data_rentgen/db/models/base.py +29 -0
- data_rentgen/db/models/dataset.py +66 -0
- data_rentgen/db/models/dataset_symlink.py +56 -0
- data_rentgen/db/models/input.py +117 -0
- data_rentgen/db/models/job.py +78 -0
- data_rentgen/db/models/location.py +66 -0
- data_rentgen/db/models/operation.py +118 -0
- data_rentgen/db/models/output.py +142 -0
- data_rentgen/db/models/run.py +175 -0
- data_rentgen/db/models/schema.py +30 -0
- data_rentgen/db/models/user.py +15 -0
- data_rentgen/db/repositories/__init__.py +2 -0
- data_rentgen/db/repositories/base.py +84 -0
- data_rentgen/db/repositories/dataset.py +120 -0
- data_rentgen/db/repositories/dataset_symlink.py +60 -0
- data_rentgen/db/repositories/input.py +214 -0
- data_rentgen/db/repositories/job.py +123 -0
- data_rentgen/db/repositories/location.py +148 -0
- data_rentgen/db/repositories/operation.py +133 -0
- data_rentgen/db/repositories/output.py +221 -0
- data_rentgen/db/repositories/run.py +207 -0
- data_rentgen/db/repositories/schema.py +43 -0
- data_rentgen/db/repositories/user.py +31 -0
- data_rentgen/db/scripts/create_partitions.py +140 -0
- data_rentgen/db/settings.py +43 -0
- data_rentgen/db/utils/fields.py +13 -0
- data_rentgen/db/utils/search.py +93 -0
- data_rentgen/db/utils/uuid.py +90 -0
- data_rentgen/dependencies/__init__.py +6 -0
- data_rentgen/dependencies/stub.py +51 -0
- data_rentgen/dto/__init__.py +39 -0
- data_rentgen/dto/dataset.py +29 -0
- data_rentgen/dto/dataset_symlink.py +38 -0
- data_rentgen/dto/input.py +50 -0
- data_rentgen/dto/job.py +39 -0
- data_rentgen/dto/location.py +25 -0
- data_rentgen/dto/operation.py +60 -0
- data_rentgen/dto/output.py +69 -0
- data_rentgen/dto/pagination.py +42 -0
- data_rentgen/dto/run.py +77 -0
- data_rentgen/dto/schema.py +24 -0
- data_rentgen/dto/user.py +22 -0
- data_rentgen/exceptions/__init__.py +15 -0
- data_rentgen/exceptions/auth.py +34 -0
- data_rentgen/exceptions/base.py +24 -0
- data_rentgen/exceptions/entity.py +46 -0
- data_rentgen/exceptions/redirect.py +21 -0
- data_rentgen/logging/__init__.py +2 -0
- data_rentgen/logging/exceptions.py +6 -0
- data_rentgen/logging/presets/colored.yml +63 -0
- data_rentgen/logging/presets/json.yml +62 -0
- data_rentgen/logging/presets/plain.yml +63 -0
- data_rentgen/logging/settings.py +88 -0
- data_rentgen/logging/setup_logging.py +29 -0
- data_rentgen/py.typed +0 -0
- data_rentgen/server/__init__.py +49 -0
- data_rentgen/server/__main__.py +35 -0
- data_rentgen/server/api/__init__.py +2 -0
- data_rentgen/server/api/handlers.py +138 -0
- data_rentgen/server/api/monitoring.py +13 -0
- data_rentgen/server/api/router.py +11 -0
- data_rentgen/server/api/v1/router/__init__.py +20 -0
- data_rentgen/server/api/v1/router/auth.py +53 -0
- data_rentgen/server/api/v1/router/dataset.py +54 -0
- data_rentgen/server/api/v1/router/job.py +54 -0
- data_rentgen/server/api/v1/router/location.py +50 -0
- data_rentgen/server/api/v1/router/operation.py +60 -0
- data_rentgen/server/api/v1/router/run.py +57 -0
- data_rentgen/server/api/v1/router/user.py +27 -0
- data_rentgen/server/errors/__init__.py +18 -0
- data_rentgen/server/errors/base.py +19 -0
- data_rentgen/server/errors/registration.py +65 -0
- data_rentgen/server/errors/schemas/__init__.py +15 -0
- data_rentgen/server/errors/schemas/invalid_request.py +30 -0
- data_rentgen/server/errors/schemas/not_authorized.py +32 -0
- data_rentgen/server/errors/schemas/not_found.py +26 -0
- data_rentgen/server/middlewares/__init__.py +34 -0
- data_rentgen/server/middlewares/application_version.py +50 -0
- data_rentgen/server/middlewares/cors.py +18 -0
- data_rentgen/server/middlewares/monitoring/__init__.py +7 -0
- data_rentgen/server/middlewares/monitoring/metrics.py +44 -0
- data_rentgen/server/middlewares/openapi.py +113 -0
- data_rentgen/server/middlewares/request_id.py +21 -0
- data_rentgen/server/middlewares/session.py +16 -0
- data_rentgen/server/middlewares/static_files.py +17 -0
- data_rentgen/server/providers/__init__.py +2 -0
- data_rentgen/server/providers/auth/__init__.py +11 -0
- data_rentgen/server/providers/auth/base_provider.py +105 -0
- data_rentgen/server/providers/auth/dummy_provider.py +99 -0
- data_rentgen/server/providers/auth/keycloak_provider.py +115 -0
- data_rentgen/server/schemas/__init__.py +8 -0
- data_rentgen/server/schemas/ping.py +12 -0
- data_rentgen/server/schemas/v1/__init__.py +69 -0
- data_rentgen/server/schemas/v1/address.py +9 -0
- data_rentgen/server/schemas/v1/auth.py +13 -0
- data_rentgen/server/schemas/v1/dataset.py +36 -0
- data_rentgen/server/schemas/v1/job.py +36 -0
- data_rentgen/server/schemas/v1/lineage.py +178 -0
- data_rentgen/server/schemas/v1/location.py +39 -0
- data_rentgen/server/schemas/v1/operation.py +113 -0
- data_rentgen/server/schemas/v1/pagination.py +54 -0
- data_rentgen/server/schemas/v1/run.py +143 -0
- data_rentgen/server/schemas/v1/user.py +8 -0
- data_rentgen/server/scripts/export_openapi_schema.py +22 -0
- data_rentgen/server/services/__init__.py +6 -0
- data_rentgen/server/services/get_user.py +30 -0
- data_rentgen/server/services/lineage.py +859 -0
- data_rentgen/server/settings/__init__.py +55 -0
- data_rentgen/server/settings/application_version.py +23 -0
- data_rentgen/server/settings/auth/__init__.py +29 -0
- data_rentgen/server/settings/auth/dummy.py +20 -0
- data_rentgen/server/settings/auth/jwt.py +43 -0
- data_rentgen/server/settings/auth/keycloak.py +37 -0
- data_rentgen/server/settings/cors.py +59 -0
- data_rentgen/server/settings/monitoring.py +67 -0
- data_rentgen/server/settings/openapi.py +149 -0
- data_rentgen/server/settings/request_id.py +37 -0
- data_rentgen/server/settings/server.py +70 -0
- data_rentgen/server/settings/session.py +59 -0
- data_rentgen/server/settings/static_files.py +35 -0
- data_rentgen/server/static/.gitkeep +0 -0
- data_rentgen/server/utils/__init__.py +2 -0
- data_rentgen/server/utils/jwt.py +25 -0
- data_rentgen/server/utils/lineage_response.py +116 -0
- data_rentgen/server/utils/slug.py +10 -0
- data_rentgen/services/__init__.py +6 -0
- data_rentgen/services/uow.py +45 -0
- data_rentgen/utils/__init__.py +5 -0
- data_rentgen/utils/uuid.py +25 -0
- data_rentgen-0.1.0.dist-info/LICENSE.txt +203 -0
- data_rentgen-0.1.0.dist-info/METADATA +121 -0
- data_rentgen-0.1.0.dist-info/RECORD +199 -0
- data_rentgen-0.1.0.dist-info/WHEEL +4 -0
- data_rentgen-0.1.0.dist-info/entry_points.txt +3 -0
data_rentgen/__init__.py
ADDED
|
@@ -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
|