Repository: litestar-org/litestar

Issue:
StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:
I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/contrib/sqlalchemy/dto.py b/litestar/contrib/sqlalchemy/dto.py
--- a/litestar/contrib/sqlalchemy/dto.py
+++ b/litestar/contrib/sqlalchemy/dto.py
@@ -1,22 +1,32 @@
 from __future__ import annotations
 
+from functools import singledispatchmethod
 from typing import TYPE_CHECKING, Generic, TypeVar
 
-from sqlalchemy import inspect, orm, sql
-from sqlalchemy.orm import DeclarativeBase, Mapped
+from sqlalchemy import Column, inspect, orm, sql
+from sqlalchemy.ext.associationproxy import AssociationProxy, AssociationProxyExtensionType
+from sqlalchemy.ext.hybrid import HybridExtensionType, hybrid_property
+from sqlalchemy.orm import (
+    ColumnProperty,
+    DeclarativeBase,
+    InspectionAttr,
+    Mapped,
+    NotExtension,
+    QueryableAttribute,
+    RelationshipProperty,
+)
 
 from litestar.dto.factory.abc import AbstractDTOFactory
-from litestar.dto.factory.field import DTO_FIELD_META_KEY
+from litestar.dto.factory.field import DTO_FIELD_META_KEY, DTOField, Mark
 from litestar.dto.factory.types import FieldDefinition
 from litestar.dto.factory.utils import get_model_type_hints
 from litestar.types.empty import Empty
 from litestar.utils.helpers import get_fully_qualified_class_name
+from litestar.utils.signature import ParsedSignature
 
 if TYPE_CHECKING:
     from typing import Any, ClassVar, Collection, Generator
 
-    from sqlalchemy import Column
-    from sqlalchemy.orm import RelationshipProperty
     from typing_extensions import TypeAlias
 
     from litestar.typing import ParsedType
@@ -26,6 +36,8 @@
 T = TypeVar("T", bound="DeclarativeBase | Collection[DeclarativeBase]")
 ElementType: TypeAlias = "Column[Any] | RelationshipProperty[Any]"
 
+SQLA_NS = {**vars(orm), **vars(sql)}
+
 
 class SQLAlchemyDTO(AbstractDTOFactory[T], Generic[T]):
     """Support for domain modelling with SQLAlchemy."""
@@ -34,40 +46,122 @@ class SQLAlchemyDTO(AbstractDTOFactory[T], Generic[T]):
 
     model_type: ClassVar[type[DeclarativeBase]]
 
+    @singledispatchmethod
+    @classmethod
+    def handle_orm_descriptor(
+        cls,
+        extension_type: NotExtension | AssociationProxyExtensionType | HybridExtensionType,
+        orm_descriptor: InspectionAttr,
+        key: str,
+        model_type_hints: dict[str, ParsedType],
+        model_name: str,
+    ) -> FieldDefinition:
+        raise NotImplementedError(f"Unsupported extension type: {extension_type}")
+
+    @handle_orm_descriptor.register(NotExtension)
+    @classmethod
+    def _(
+        cls,
+        extension_type: NotExtension,
+        key: str,
+        orm_descriptor: InspectionAttr,
+        model_type_hints: dict[str, ParsedType],
+        model_name: str,
+    ) -> FieldDefinition:
+        if not isinstance(orm_descriptor, QueryableAttribute):
+            raise NotImplementedError(f"Unexpected descriptor type for '{extension_type}': '{orm_descriptor}'")
+
+        elem: ElementType
+        if isinstance(orm_descriptor.property, ColumnProperty):
+            if not isinstance(orm_descriptor.property.expression, Column):
+                raise NotImplementedError(f"Expected 'Column', got: '{orm_descriptor.property.expression}'")
+            elem = orm_descriptor.property.expression
+        elif isinstance(orm_descriptor.property, RelationshipProperty):
+            elem = orm_descriptor.property
+        else:
+            raise NotImplementedError(f"Unhandled property type: '{orm_descriptor.property}'")
+
+        default, default_factory = _detect_defaults(elem)
+
+        if (parsed_type := model_type_hints[key]).origin is Mapped:
+            (parsed_type,) = parsed_type.inner_types
+        else:
+            raise NotImplementedError(f"Expected 'Mapped' origin, got: '{parsed_type.origin}'")
+
+        return FieldDefinition(
+            name=key,
+            default=default,
+            parsed_type=parsed_type,
+            default_factory=default_factory,
+            dto_field=elem.info.get(DTO_FIELD_META_KEY),
+            unique_model_name=model_name,
+        )
+
+    @handle_orm_descriptor.register(AssociationProxyExtensionType)
+    @classmethod
+    def _(
+        cls,
+        extension_type: AssociationProxyExtensionType,
+        key: str,
+        orm_descriptor: InspectionAttr,
+        model_type_hints: dict[str, ParsedType],
+        model_name: str,
+    ) -> FieldDefinition:
+        if not isinstance(orm_descriptor, AssociationProxy):
+            raise NotImplementedError(f"Unexpected descriptor type '{orm_descriptor}' for '{extension_type}'")
+
+        if (parsed_type := model_type_hints[key]).origin is AssociationProxy:
+            (parsed_type,) = parsed_type.inner_types
+        else:
+            raise NotImplementedError(f"Expected 'AssociationProxy' origin, got: '{parsed_type.origin}'")
+
+        return FieldDefinition(
+            name=key,
+            default=Empty,
+            parsed_type=parsed_type,
+            default_factory=None,
+            dto_field=orm_descriptor.info.get(DTO_FIELD_META_KEY, DTOField(mark=Mark.READ_ONLY)),
+            unique_model_name=model_name,
+        )
+
+    @handle_orm_descriptor.register(HybridExtensionType)
+    @classmethod
+    def _(
+        cls,
+        extension_type: HybridExtensionType,
+        key: str,
+        orm_descriptor: InspectionAttr,
+        model_type_hints: dict[str, ParsedType],
+        model_name: str,
+    ) -> FieldDefinition:
+        if not isinstance(orm_descriptor, hybrid_property):
+            raise NotImplementedError(f"Unexpected descriptor type '{orm_descriptor}' for '{extension_type}'")
+
+        getter_sig = ParsedSignature.from_fn(orm_descriptor.fget, {})
+
+        return FieldDefinition(
+            name=key,
+            default=Empty,
+            parsed_type=getter_sig.return_type,
+            default_factory=None,
+            dto_field=orm_descriptor.info.get(DTO_FIELD_META_KEY, DTOField(mark=Mark.READ_ONLY)),
+            unique_model_name=model_name,
+        )
+
     @classmethod
     def generate_field_definitions(cls, model_type: type[DeclarativeBase]) -> Generator[FieldDefinition, None, None]:
         if (mapper := inspect(model_type)) is None:  # pragma: no cover
             raise RuntimeError("Unexpected `None` value for mapper.")
 
-        columns = mapper.columns
-        relationships = mapper.relationships
-
         # includes SQLAlchemy names and other mapped class names in the forward reference resolution namespace
-        namespace = dict(vars(orm))
-        namespace.update(vars(sql))
-        namespace.update({m.class_.__name__: m.class_ for m in mapper.registry.mappers if m is not mapper})
-
-        for key, parsed_type in get_model_type_hints(model_type, namespace=namespace).items():
-            elem: ElementType | None
-            elem = columns.get(key, relationships.get(key))  # pyright:ignore
-            if elem is None:
-                continue
-
-            if parsed_type.origin is Mapped:
-                (parsed_type,) = parsed_type.inner_types
-
-            default, default_factory = _detect_defaults(elem)
-
-            field_def = FieldDefinition(
-                name=key,
-                default=default,
-                parsed_type=parsed_type,
-                default_factory=default_factory,
-                dto_field=elem.info.get(DTO_FIELD_META_KEY),
-                unique_model_name=get_fully_qualified_class_name(model_type),
-            )
+        namespace = {**SQLA_NS, **{m.class_.__name__: m.class_ for m in mapper.registry.mappers if m is not mapper}}
+        model_type_hints = get_model_type_hints(model_type, namespace=namespace)
+        model_name = get_fully_qualified_class_name(model_type)
 
-            yield field_def
+        for key, orm_descriptor in mapper.all_orm_descriptors.items():
+            yield cls.handle_orm_descriptor(
+                orm_descriptor.extension_type, key, orm_descriptor, model_type_hints, model_name
+            )
 
     @classmethod
     def detect_nested_field(cls, parsed_type: ParsedType) -> bool:
diff --git a/litestar/dto/factory/_backends/utils.py b/litestar/dto/factory/_backends/utils.py
--- a/litestar/dto/factory/_backends/utils.py
+++ b/litestar/dto/factory/_backends/utils.py
@@ -232,10 +232,12 @@ def transfer_type_data(
         return transfer_nested_simple_type_data(dest_type, transfer_type.nested_field_info, dto_for, source_value)
     if isinstance(transfer_type, UnionType) and transfer_type.has_nested:
         return transfer_nested_union_type_data(transfer_type, dto_for, source_value)
-    if isinstance(transfer_type, CollectionType) and transfer_type.has_nested:
-        return transfer_nested_collection_type_data(
-            transfer_type.parsed_type.origin, transfer_type, dto_for, source_value
-        )
+    if isinstance(transfer_type, CollectionType):
+        if transfer_type.has_nested:
+            return transfer_nested_collection_type_data(
+                transfer_type.parsed_type.origin, transfer_type, dto_for, source_value
+            )
+        return transfer_type.parsed_type.origin(source_value)
     return source_value
 
 


Test patch:
diff --git a/tests/contrib/sqlalchemy/test_dto_integration.py b/tests/contrib/sqlalchemy/test_dto_integration.py
--- a/tests/contrib/sqlalchemy/test_dto_integration.py
+++ b/tests/contrib/sqlalchemy/test_dto_integration.py
@@ -1,4 +1,5 @@
 from dataclasses import dataclass
+from types import ModuleType
 from typing import Any, Callable, Dict, List, Tuple
 
 import pytest
@@ -134,3 +135,101 @@ def get_handler() -> Book:
 
         response_callback = client.post("/", json=json_data)
         assert response_callback.json() == json_data
+
+
+def test_dto_with_association_proxy(create_module: Callable[[str], ModuleType]) -> None:
+    module = create_module(
+        """
+from __future__ import annotations
+
+from typing import Final, List
+
+from sqlalchemy import Column
+from sqlalchemy import ForeignKey
+from sqlalchemy import Integer
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+from sqlalchemy.orm import relationship
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.ext.associationproxy import AssociationProxy
+
+from litestar import get
+from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
+from litestar.dto.factory import dto_field
+
+class Base(DeclarativeBase):
+    pass
+
+class User(Base):
+    __tablename__ = "user"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    kw: Mapped[List[Keyword]] = relationship(secondary=lambda: user_keyword_table, info=dto_field("private"))
+    # proxy the 'keyword' attribute from the 'kw' relationship
+    keywords: AssociationProxy[List[str]] = association_proxy("kw", "keyword")
+
+class Keyword(Base):
+    __tablename__ = "keyword"
+    id: Mapped[int] = mapped_column(primary_key=True)
+    keyword: Mapped[str] = mapped_column(String(64))
+
+user_keyword_table: Final[Table] = Table(
+    "user_keyword",
+    Base.metadata,
+    Column("user_id", Integer, ForeignKey("user.id"), primary_key=True),
+    Column("keyword_id", Integer, ForeignKey("keyword.id"), primary_key=True),
+)
+
+dto = SQLAlchemyDTO[User]
+
+@get("/", return_dto=dto)
+def get_handler() -> User:
+    return User(id=1, kw=[Keyword(keyword="bar"), Keyword(keyword="baz")])
+"""
+    )
+
+    with create_test_client(route_handlers=[module.get_handler], debug=True) as client:
+        response = client.get("/")
+        assert response.json() == {"id": 1, "keywords": ["bar", "baz"]}
+
+
+def test_dto_with_hybrid_property(create_module: Callable[[str], ModuleType]) -> None:
+    module = create_module(
+        """
+from __future__ import annotations
+
+from sqlalchemy.ext.hybrid import hybrid_property
+from sqlalchemy.orm import DeclarativeBase
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
+
+from litestar import get
+from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
+
+class Base(DeclarativeBase):
+    pass
+
+class Interval(Base):
+    __tablename__ = 'interval'
+
+    id: Mapped[int] = mapped_column(primary_key=True)
+    start: Mapped[int]
+    end: Mapped[int]
+
+    @hybrid_property
+    def length(self) -> int:
+        return self.end - self.start
+
+dto = SQLAlchemyDTO[Interval]
+
+@get("/", return_dto=dto)
+def get_handler() -> Interval:
+    return Interval(id=1, start=1, end=3)
+"""
+    )
+
+    with create_test_client(route_handlers=[module.get_handler], debug=True) as client:
+        response = client.get("/")
+        assert response.json() == {"id": 1, "start": 1, "end": 3, "length": 2}