Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ jobs:
suite:
- name: pipelines
args: tests/cdm_data_loaders/pipelines/
- name: docker
args: --run-docker -m "docker and not requires_spark and not requires_ceph" tests/cdm_data_loaders/pipelines/
# - name: docker
# args: --run-docker -m "docker and not requires_spark and not requires_ceph" tests/cdm_data_loaders/pipelines/
- name: integration
args: tests/integration/
- name: everything else
Expand Down
1,122 changes: 1,122 additions & 0 deletions docs/xsd_to_pyarrow_specification.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pdb_manifest = "cdm_data_loaders.pipelines.pdb_manifest:cli"
uniprot_cdm = "cdm_data_loaders.pipelines.uniprot_kb_cdm:cli"
uniref_cdm = "cdm_data_loaders.pipelines.uniref_cdm:cli"
xmltodict = "cdm_data_loaders.pipelines.xmltodict.pipeline:cli"
xmltopyarrow = "cdm_data_loaders.pipelines.xmltopyarrow.pipeline:cli"
xml2db = "cdm_data_loaders.pipelines.xml2db.pipeline:cli"
xsv = "cdm_data_loaders.pipelines.xsv.pipeline:cli"
hello_world = "cdm_data_loaders.utils.hello_world:cli"
Expand Down
3 changes: 0 additions & 3 deletions src/cdm_data_loaders/pipelines/ncbi_ftp_promote.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
final location in the Lakehouse.
"""

import logging
from pathlib import Path, PurePosixPath
from typing import Final

Expand All @@ -22,8 +21,6 @@
DEFAULT_TRANSFER_MANIFEST_FILE: PurePosixPath = PurePosixPath("transfer_manifest.txt")
ESTIMATED_FILES_PER_ASSEMBLY: int = 21

logger = logging.getLogger("dlt")


class PromoteSettings(LoggerSettings):
"""Configuration for the NCBI FTP file promotion pipeline."""
Expand Down
12 changes: 5 additions & 7 deletions src/cdm_data_loaders/pipelines/xmltodict/pipeline.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""XML to dictionary pipeline for the KBase CTS."""

from collections.abc import Generator, Iterator
from logging import Logger, getLogger
from pathlib import Path
from typing import TYPE_CHECKING, Any

Expand All @@ -18,15 +17,15 @@
if TYPE_CHECKING:
from dlt.extract import DltResource

logger: Logger = getLogger(__name__)


def _read_items(items: Iterator[FileItemDict], settings: XmlToDictSettings) -> Generator[TDataItems, Any, Any]:
def _read_xmltodict_items(

Check warning on line 21 in src/cdm_data_loaders/pipelines/xmltodict/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltodict/pipeline.py#L21

Added line #L21 was not covered by tests
items: Iterator[FileItemDict], settings: XmlToDictSettings
) -> Generator[TDataItems, Any, Any]:
"""Read each file in items. Yields one dict per `xml_tag` element.

:param items: file items to read
:type items: Iterator[FileItemDict]
:param settings: pipeline config, including xml_tag and xsd-derived paths
:param settings: pipeline config, including xml_tag
:type settings: XmlToDictSettings
:yield: one dict per matched element
:rtype: Generator[TDataItems, Any, Any]
Expand All @@ -41,8 +40,7 @@
:param settings: pipeline configuration
:type settings: XmlToDictSettings
"""
reader = dlt.transformer(_read_items, name="xmltodict_reader", parallelized=True)

reader = dlt.transformer(_read_xmltodict_items, name="xmltodict_reader", parallelized=True)

Check warning on line 43 in src/cdm_data_loaders/pipelines/xmltodict/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltodict/pipeline.py#L43

Added line #L43 was not covered by tests
files = filesystem(bucket_url=str(settings.input_dir), file_glob=settings.file_glob)

xmltodict_resource: DltResource = files | reader(settings)
Expand Down
51 changes: 2 additions & 49 deletions src/cdm_data_loaders/pipelines/xmltodict/settings.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""Settings class for the XML to dictionary pipeline for the KBase CTS."""

from functools import cached_property
from typing import Annotated, Any, Final, Self
from typing import Annotated, Final

from pydantic import Field, FilePath, PrivateAttr, computed_field, model_validator
from pydantic import Field
from pydantic_settings import SettingsConfigDict

from cdm_data_loaders.core.fields import (
Expand All @@ -17,7 +16,6 @@
FileFormatIngestSettings,
default_settings_with_shortcuts,
)
from cdm_data_loaders.readers.xsd import find_list_and_single_child_paths, load_schema

PIPELINE_NAME: Final[str] = "xmltodict"

Expand All @@ -33,9 +31,6 @@ class XmlToDictSettings(FileFormatIngestSettings):
},
)

_paths: Annotated[dict[str, Any], Field(description="parent-child paths in the schema")] = PrivateAttr(
default_factory=dict
)
file_glob: Annotated[
FileGlob,
Field(
Expand All @@ -50,45 +45,3 @@ class XmlToDictSettings(FileFormatIngestSettings):
description="XML tag to capture the contents of",
),
]
xsd_file: Annotated[
FilePath | None,
Field(
None,
description="Path to the XSD file describing the XML documents to be imported. Including the schema will ensure that the appropriate relationships (1-to-1 vs 1-to-many) are created between parent and child elements.",
),
]

@model_validator(mode="after")
def check_xsd_file(self) -> Self:
"""Check whether the schema file is present and correct; if so, init self._paths."""
if self.xsd_file:
try:
schema = load_schema(self.xsd_file)
list_paths, single_paths = find_list_and_single_child_paths(schema)
self._paths = {"list": list_paths, "single": single_paths}
except Exception as e:
err_msg = f"Could not generate parent-child relationships for schema: {e!s}"
raise ValueError(err_msg) from e

return self

@computed_field
@cached_property
def xmltodict_args(self) -> dict[str, Any]:
"""Extra parser configuration arguments for running xmltodict over a dataset.

:return: kwargs for configuring xmltodict.parse()
:rtype: dict[str, Any]
"""
if not self._paths:
return {}

single_paths = self._paths["single"]

def force_list(path: list[tuple[str, str | None]], key: str, _: Any) -> bool:
if not path:
return False

return (path[-1][0], key) not in single_paths

return {"force_list": force_list}
Empty file.
63 changes: 63 additions & 0 deletions src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""XML to pyarrow pipeline for the KBase CTS."""

from collections.abc import Generator, Iterator
from pathlib import Path
from typing import TYPE_CHECKING, Any

Check warning on line 5 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L3-L5

Added lines #L3 - L5 were not covered by tests

import dlt
from dlt.common.pipeline import LoadInfo
from dlt.common.storages.fsspec_filesystem import FileItemDict
from dlt.common.typing import TDataItems
from dlt.sources.filesystem import filesystem

Check warning on line 11 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L7-L11

Added lines #L7 - L11 were not covered by tests

from cdm_data_loaders.pipelines.core import PipelineCli, run_pipeline
from cdm_data_loaders.pipelines.xmltopyarrow.settings import PIPELINE_NAME, XmlToPyArrowSettings
from cdm_data_loaders.readers.xml import process_xml_file_to_pyarrow

Check warning on line 15 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L13-L15

Added lines #L13 - L15 were not covered by tests

if TYPE_CHECKING:
from dlt.extract import DltResource


def _read_items(items: Iterator[FileItemDict], settings: XmlToPyArrowSettings) -> Generator[TDataItems, Any, Any]:

Check warning on line 21 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L21

Added line #L21 was not covered by tests
"""Read each file in items. Yields PyArrow tables for matched `xml_tag` elements.

:param items: file items to read
:type items: Iterator[FileItemDict]
:param settings: pipeline config, including xml_tag and XSD schema
:type settings: XmlToPyArrowSettings
:yield: PyArrow tables of matched elements
:rtype: Generator[TDataItems, Any, Any]
"""
for file_item in items:
yield from process_xml_file_to_pyarrow(settings, file_path=Path(file_item.local_file_path))

Check warning on line 32 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L31-L32

Added lines #L31 - L32 were not covered by tests


def run_xmltopyarrow_pipeline(settings: XmlToPyArrowSettings) -> LoadInfo | None:

Check warning on line 35 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L35

Added line #L35 was not covered by tests
"""Run the XML to PyArrow pipeline on matching files in settings.input_dir.

:param settings: pipeline configuration
:type settings: XmlToPyArrowSettings
"""
reader = dlt.transformer(_read_items, name="xmltopyarrow_reader", parallelized=True)
files = filesystem(bucket_url=str(settings.input_dir), file_glob=settings.file_glob)

Check warning on line 42 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L41-L42

Added lines #L41 - L42 were not covered by tests

xmltopyarrow_resource: DltResource = files | reader(settings)

Check warning on line 44 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L44

Added line #L44 was not covered by tests

return run_pipeline(

Check warning on line 46 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L46

Added line #L46 was not covered by tests
settings=settings,
resource=xmltopyarrow_resource,
pipeline_kwargs={
"pipeline_name": PIPELINE_NAME,
"dataset_name": settings.dataset_name,
},
pipeline_run_kwargs={
"loader_file_format": str(settings.loader_file_format),
},
)


cli = PipelineCli(XmlToPyArrowSettings, run_xmltopyarrow_pipeline)

Check warning on line 59 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L59

Added line #L59 was not covered by tests


if __name__ == "__main__":
cli()

Check warning on line 63 in src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/pipeline.py#L62-L63

Added lines #L62 - L63 were not covered by tests
95 changes: 95 additions & 0 deletions src/cdm_data_loaders/pipelines/xmltopyarrow/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Settings class for the XML to PyArrow pipeline for the KBase CTS."""

from typing import Annotated, Final, Self

import pyarrow as pa
import xmlschema
from pydantic import Field, FilePath, PositiveInt, PrivateAttr, model_validator
from pydantic_settings import SettingsConfigDict

from cdm_data_loaders.core.fields import (
DEFAULT_XML_FILE_GLOB,
FILE_GLOB,
FileGlob,
NonEmptyStr,
TableName,
)
from cdm_data_loaders.core.settings import (
FileFormatIngestSettings,
default_settings_with_shortcuts,
)
from cdm_data_loaders.readers.xsd import (
DEFAULT_MAX_RECURSION_DEPTH,
generate_pyarrow_schema,
load_schema,
)

PIPELINE_NAME: Final[str] = "xmltopyarrow"


class XmlToPyArrowSettings(FileFormatIngestSettings):
"""Settings for the XML to PyArrow ingestion pipeline."""

model_config: SettingsConfigDict = default_settings_with_shortcuts(
cli_prog_name=PIPELINE_NAME,
cli_shortcuts={
**FileFormatIngestSettings.model_config["cli_shortcuts"],
FILE_GLOB: "g",
},
)

_xsd_schema: xmlschema.XMLSchema = PrivateAttr()
_pyarrow_schema: pa.Schema = PrivateAttr()
file_glob: Annotated[
FileGlob,
Field(
default=DEFAULT_XML_FILE_GLOB,
description="Glob pattern for XML files inside each entity's input subdirectory.",
),
]
table_name: TableName
xml_tag: Annotated[
NonEmptyStr,
Field(
description="XML tag to capture the contents of",
),
]
xsd_file: Annotated[
FilePath,
Field(
description="Path to the XSD file describing the XML documents to be imported. Including the schema will ensure that the appropriate relationships (1-to-1 vs 1-to-many) are created between parent and child elements, and that element text is stored consistently.",
),
]
max_recursion_depth: Annotated[
PositiveInt,
Field(
default=DEFAULT_MAX_RECURSION_DEPTH,
description="Maximum recursion depth for self-referential XSD types before serializing deeper content as JSON.",
),
]

@model_validator(mode="after")
def check_xsd_file(self) -> Self:
"""Check whether the schema file is present and valid; load the XSD and generate the PyArrow schema."""
try:
self._xsd_schema = load_schema(self.xsd_file)
self._pyarrow_schema = generate_pyarrow_schema(
self._xsd_schema,
self.xml_tag,
max_recursion_depth=self.max_recursion_depth,
)
except Exception as e:
err_msg = f"Could not generate PyArrow schema from XSD: {e!s}"
raise ValueError(err_msg) from e

Check warning on line 83 in src/cdm_data_loaders/pipelines/xmltopyarrow/settings.py

View check run for this annotation

Codecov / codecov/patch

src/cdm_data_loaders/pipelines/xmltopyarrow/settings.py#L81-L83

Added lines #L81 - L83 were not covered by tests

return self

@property
def xsd_schema(self) -> xmlschema.XMLSchema:
"""Return the loaded XMLSchema instance."""
return self._xsd_schema

@property
def pyarrow_schema(self) -> pa.Schema:
"""Return the generated PyArrow schema for xml_tag."""
return self._pyarrow_schema
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import subprocess
from dataclasses import dataclass
from functools import cached_property
from logging import Logger, getLogger
from pathlib import Path
from shutil import copy, move
from typing import Annotated, Final, Self
Expand Down Expand Up @@ -35,9 +34,6 @@
VALID_SUFFIX: Final[str] = "-valid"


logger: Logger = getLogger(__name__)


class ErrorRecord(BaseModel):
"""A single error encountered while cleaning/validating a file.

Expand Down
Loading
Loading