Source code for edri.integrations.sqlalchemy.path
from __future__ import annotations
from typing import Any
from edri.utility.function import camel2snake
from sqlalchemy import inspect as sqlalchemy_inspect
__all__ = ["SQLAlchemyPathError", "canonical_orm_path"]
[docs]
class SQLAlchemyPathError(ValueError):
"""Raised when a public dotted path does not match mapped ORM attributes."""
[docs]
def canonical_orm_path(
model: type[Any],
public_path: str,
*,
require_scalar: bool = False,
) -> tuple[str, ...]:
"""Resolve a camelCase dotted path to strict mapped attribute names.
Every non-final segment must be a relationship. A column may only occur as
the final segment. When ``require_scalar`` is true, a relationship may not
be the final segment either.
"""
if not isinstance(public_path, str) or not public_path.strip():
raise SQLAlchemyPathError("ORM path cannot be empty")
public_segments = public_path.split(".")
current_model = model
result: list[str] = []
for index, public_segment in enumerate(public_segments):
segment = public_segment.strip()
if not segment:
raise SQLAlchemyPathError(f'Invalid ORM path "{public_path}"')
mapper = sqlalchemy_inspect(current_model)
attribute = camel2snake(segment)
if attribute == "id" and len(mapper.primary_key) == 1:
attribute = mapper.primary_key[0].key
is_last = index == len(public_segments) - 1
if attribute in mapper.column_attrs:
if not is_last:
raise SQLAlchemyPathError(
f'Cannot traverse scalar attribute "{attribute}" in "{public_path}"'
)
result.append(attribute)
continue
relationship = mapper.relationships.get(attribute)
if relationship is None:
raise SQLAlchemyPathError(
f'Unknown mapped attribute "{attribute}" in "{public_path}"'
)
if is_last and require_scalar:
raise SQLAlchemyPathError(
f'ORM path "{public_path}" ends in a relationship'
)
result.append(attribute)
current_model = relationship.mapper.class_
return tuple(result)