Source code for bridge.pipelines.protocols.map
"""
Abstract class for mapping between repository and metadata models.
"""
import importlib
from abc import ABC, abstractmethod
from collections.abc import Callable
from enum import Enum
from functools import partial
from typing import Any
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator
from bridge.utils import maybe_await
from .none_propagation import SafeAttr, deep_unwrap
[docs]
class Method(Enum):
"""
Matching method for mapping metadata record from one platform to another (GitHub, bio.tools).
"""
EXACT = "exact"
SUBSET = "subset"
FUZZY = "fuzzy"
[docs]
class ModelsMap(ABC):
"""
Abstract class for mapping between repository and metadata models.
Parameters
----------
repo: Any
A repository model instance.
metadata: Any
A metadata model instance.
"""
def __init__(self, repo: Any, metadata: Any):
super().__init__()
self._repo_raw = repo
self._metadata_raw = metadata
# expose safe proxies for all downstream attribute chains
self.repo = SafeAttr(repo)
self.metadata = SafeAttr(metadata)
@property
@abstractmethod
def map(self) -> dict[str, Any]:
"""
Perform the mapping between repository and metadata models.
Return
-------
Any
The map representation of the models.
"""
return
def _to_path(fn):
if isinstance(fn, partial):
raise TypeError("partials aren’t serializable by simple dotted path")
mod = getattr(fn, "__module__", "")
qn = getattr(fn, "__qualname__", "")
if not (mod and qn):
raise TypeError("callable lacks module/qualname")
if mod == "__main__" or "<locals>" in qn:
raise TypeError("callable must be a top-level, importable symbol")
return f"{mod}:{qn}"
def _import_from_path(path: str):
# Supports "pkg.mod:attr.nested" or "pkg.mod.attr.nested"
sep = ":" if ":" in path else "."
mod_path, attr = path.rsplit(sep, 1)
mod = importlib.import_module(mod_path)
obj = mod
for part in attr.split("."):
obj = getattr(obj, part)
if not callable(obj):
raise TypeError(f"{path!r} is not callable")
return obj
[docs]
class MapItem(BaseModel):
"""
Map metadata property to corresponding repository metadata property and match method.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
schema_entry: Any
repo_entry: Any
method: Method | None
fn: Callable[[Any, Any], Any] | None = None
[docs]
@field_validator("fn", mode="before")
@classmethod
def ensure_callable_or_none(cls, v):
"""
Validate that fn is either None, a callable, or a dotted path to a callable.
"""
if v is None:
return None
if isinstance(v, str):
v = _import_from_path(v)
if not callable(v):
raise TypeError("fn must be None, a callable, or a dotted path to one")
return v
[docs]
@field_serializer("fn")
def serialize_fn(self, fn):
"""
Serialize fn to a dotted path.
"""
if fn is None:
return None
return _to_path(fn)
[docs]
async def run(self) -> Any:
"""
Run the mapping function if provided, otherwise return None.
Returns
-------
Any
The result of the mapping function or None.
"""
if not self.fn:
return None
repo = deep_unwrap(self.repo_entry)
schema = deep_unwrap(self.schema_entry)
return await maybe_await(self.fn, repo, schema)