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/dto/factory/_backends/abc.py b/litestar/dto/factory/_backends/abc.py
--- a/litestar/dto/factory/_backends/abc.py
+++ b/litestar/dto/factory/_backends/abc.py
@@ -10,7 +10,13 @@
 from litestar._signature.field import SignatureField
 
 from .types import NestedFieldDefinition, TransferFieldDefinition
-from .utils import RenameStrategies, build_annotation_for_backend, get_model_type, should_exclude_field
+from .utils import (
+    RenameStrategies,
+    build_annotation_for_backend,
+    generate_reverse_name_map,
+    get_model_type,
+    should_exclude_field,
+)
 
 if TYPE_CHECKING:
     from typing import AbstractSet, Any, Callable, Final, Generator
@@ -89,9 +95,7 @@ def __init__(self, context: BackendContext) -> None:
         """
         self.context = context
         self.parsed_field_definitions = self.parse_model(context.model_type, context.config.exclude)
-        self.reverse_name_map = {
-            f.serialization_name: f.name for f in self.parsed_field_definitions.values() if f.serialization_name
-        }
+        self.reverse_name_map = generate_reverse_name_map(self.parsed_field_definitions)
         self.data_container_type = self.create_data_container_type(context)
         self.annotation = build_annotation_for_backend(context.parsed_type.annotation, self.data_container_type)
 
diff --git a/litestar/dto/factory/_backends/msgspec/utils.py b/litestar/dto/factory/_backends/msgspec/utils.py
--- a/litestar/dto/factory/_backends/msgspec/utils.py
+++ b/litestar/dto/factory/_backends/msgspec/utils.py
@@ -144,22 +144,23 @@ def _build_struct_from_model(model: Any, struct_type: type[StructT], reverse_nam
         model_name = reverse_name_map.get(key, key)
         model_val = getattr(model, model_name)
         if parsed_type.is_subclass_of(Struct):
-            data[key] = _build_struct_from_model(model_val, parsed_type.annotation, {})
+            data[key] = _build_struct_from_model(model_val, parsed_type.annotation, reverse_name_map)
         elif parsed_type.is_union:
-            data[key] = _handle_union_type(parsed_type, model_val)
+            data[key] = _handle_union_type(parsed_type, model_val, reverse_name_map)
         elif parsed_type.is_collection:
-            data[key] = _handle_collection_type(parsed_type, model_val)
+            data[key] = _handle_collection_type(parsed_type, model_val, reverse_name_map)
         else:
             data[key] = model_val
     return struct_type(**data)
 
 
-def _handle_union_type(parsed_type: ParsedType, model_val: Any) -> Any:
+def _handle_union_type(parsed_type: ParsedType, model_val: Any, reverse_name_map: dict[str, str]) -> Any:
     """Handle union type.
 
     Args:
         parsed_type: Parsed type.
         model_val: Model value.
+        reverse_name_map: reverse name map for field definitions.
 
     Returns:
         Model value.
@@ -172,22 +173,25 @@ def _handle_union_type(parsed_type: ParsedType, model_val: Any) -> Any:
             # for the nested model type instance. For the most likely case of an optional union of a single
             # nested type, this should be sufficient.
             try:
-                return _build_struct_from_model(model_val, inner_type.annotation, {})
+                return _build_struct_from_model(model_val, inner_type.annotation, reverse_name_map)
             except (AttributeError, TypeError):
                 continue
     return model_val
 
 
-def _handle_collection_type(parsed_type: ParsedType, model_val: Any) -> Any:
+def _handle_collection_type(parsed_type: ParsedType, model_val: Any, reverse_name_map: dict[str, str]) -> Any:
     """Handle collection type.
 
     Args:
         parsed_type: Parsed type.
         model_val: Model value.
+        reverse_name_map: reverse name map for field definitions.
 
     Returns:
         Model value.
     """
     if parsed_type.inner_types and (inner_type := parsed_type.inner_types[0]).is_subclass_of(Struct):
-        return parsed_type.origin(_build_struct_from_model(m, inner_type.annotation, {}) for m in model_val)
+        return parsed_type.origin(
+            _build_struct_from_model(m, inner_type.annotation, reverse_name_map) for m in model_val
+        )
     return model_val
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
@@ -8,12 +8,16 @@
 from litestar.types.builtin_types import NoneType
 from litestar.utils.signature import ParsedType
 
+from .types import TransferFieldDefinition
+
 if TYPE_CHECKING:
     from typing import AbstractSet, Any, Iterable
 
     from litestar.dto.factory.types import FieldDefinition, RenameStrategy
     from litestar.dto.types import ForType
 
+    from .types import FieldDefinitionsType, NestedFieldDefinition
+
 __all__ = (
     "RenameStrategies",
     "build_annotation_for_backend",
@@ -123,3 +127,22 @@ def _camelize(string: str, capitalize_first_letter: bool = False) -> str:
             word if index == 0 and not capitalize_first_letter else word.capitalize()
             for index, word in enumerate(string.split("_"))
         )
+
+
+def generate_reverse_name_map(field_definitions: FieldDefinitionsType) -> dict[str, str]:
+    result = {}
+    for field_definition in field_definitions.values():
+        result.update(_generate_reverse_name_map(field_definition))
+
+    return result
+
+
+def _generate_reverse_name_map(field_definition: TransferFieldDefinition | NestedFieldDefinition) -> dict[str, str]:
+    if isinstance(field_definition, TransferFieldDefinition):
+        return (
+            {field_definition.serialization_name: field_definition.name} if field_definition.serialization_name else {}
+        )
+
+    return generate_reverse_name_map(
+        {field_definition.name: field_definition.field_definition, **field_definition.nested_field_definitions}
+    )


Test patch:
diff --git a/tests/contrib/sqlalchemy/test_dto_integration.py b/tests/contrib/sqlalchemy/test_dto_integration.py
new file mode 100644
--- /dev/null
+++ b/tests/contrib/sqlalchemy/test_dto_integration.py
@@ -0,0 +1,136 @@
+from dataclasses import dataclass
+from typing import Any, Callable, Dict, List, Tuple
+
+import pytest
+from sqlalchemy import ForeignKey, String
+from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column, relationship
+from typing_extensions import Annotated
+
+from litestar import get, post
+from litestar.contrib.sqlalchemy.dto import SQLAlchemyDTO
+from litestar.dto.factory import DTOConfig
+from litestar.dto.factory._backends.utils import RenameStrategies
+from litestar.dto.factory.types import RenameStrategy
+from litestar.testing import create_test_client
+
+
+class Base(DeclarativeBase):
+    id: Mapped[str] = mapped_column(primary_key=True)
+
+    # noinspection PyMethodParameters
+    @declared_attr.directive
+    def __tablename__(cls) -> str:  # pylint: disable=no-self-argument
+        """Infer table name from class name."""
+        return cls.__name__.lower()
+
+
+class Author(Base):
+    name: Mapped[str] = mapped_column(default="Arthur")
+    date_of_birth: Mapped[str] = mapped_column(nullable=True)
+
+
+class BookReview(Base):
+    review: Mapped[str]
+    book_id: Mapped[str] = mapped_column(ForeignKey("book.id"), default="000")
+
+
+class Book(Base):
+    title: Mapped[str] = mapped_column(String(length=250), default="Hi")
+    author_id: Mapped[str] = mapped_column(ForeignKey("author.id"), default="123")
+    first_author: Mapped[Author] = relationship(lazy="joined", innerjoin=True)
+    reviews: Mapped[List[BookReview]] = relationship(lazy="joined", innerjoin=True)
+    bar: Mapped[str] = mapped_column(default="Hello")
+    SPAM: Mapped[str] = mapped_column(default="Bye")
+    spam_bar: Mapped[str] = mapped_column(default="Goodbye")
+
+
+@dataclass
+class BookAuthorTestData:
+    book_id: str = "000"
+    book_title: str = "TDD Python"
+    book_author_id: str = "123"
+    book_author_name: str = "Harry Percival"
+    book_author_date_of_birth: str = "01/01/1900"
+    book_bar: str = "Hi"
+    book_SPAM: str = "Bye"
+    book_spam_bar: str = "GoodBye"
+    book_review_id: str = "23432"
+    book_review: str = "Excellent!"
+
+
+@pytest.fixture
+def book_json_data() -> Callable[[RenameStrategy, BookAuthorTestData], Tuple[Dict[str, Any], Book]]:
+    def _generate(rename_strategy: RenameStrategy, test_data: BookAuthorTestData) -> Tuple[Dict[str, Any], Book]:
+        data: Dict[str, Any] = {
+            RenameStrategies(rename_strategy)("id"): test_data.book_id,
+            RenameStrategies(rename_strategy)("title"): test_data.book_title,
+            RenameStrategies(rename_strategy)("author_id"): test_data.book_author_id,
+            RenameStrategies(rename_strategy)("bar"): test_data.book_bar,
+            RenameStrategies(rename_strategy)("SPAM"): test_data.book_SPAM,
+            RenameStrategies(rename_strategy)("spam_bar"): test_data.book_spam_bar,
+            RenameStrategies(rename_strategy)("first_author"): {
+                RenameStrategies(rename_strategy)("id"): test_data.book_author_id,
+                RenameStrategies(rename_strategy)("name"): test_data.book_author_name,
+                RenameStrategies(rename_strategy)("date_of_birth"): test_data.book_author_date_of_birth,
+            },
+            RenameStrategies(rename_strategy)("reviews"): [
+                {
+                    RenameStrategies(rename_strategy)("book_id"): test_data.book_id,
+                    RenameStrategies(rename_strategy)("id"): test_data.book_review_id,
+                    RenameStrategies(rename_strategy)("review"): test_data.book_review,
+                }
+            ],
+        }
+        book = Book(
+            id=test_data.book_id,
+            title=test_data.book_title,
+            author_id=test_data.book_author_id,
+            bar=test_data.book_bar,
+            SPAM=test_data.book_SPAM,
+            spam_bar=test_data.book_spam_bar,
+            first_author=Author(
+                id=test_data.book_author_id,
+                name=test_data.book_author_name,
+                date_of_birth=test_data.book_author_date_of_birth,
+            ),
+            reviews=[
+                BookReview(id=test_data.book_review_id, review=test_data.book_review, book_id=test_data.book_id),
+            ],
+        )
+        return data, book
+
+    return _generate
+
+
+@pytest.mark.parametrize(
+    "rename_strategy",
+    [
+        ("camel"),
+    ],
+)
+def test_fields_alias_generator_sqlalchemy(
+    rename_strategy: RenameStrategy,
+    book_json_data: Callable[[RenameStrategy, BookAuthorTestData], Tuple[Dict[str, Any], Book]],
+) -> None:
+    test_data = BookAuthorTestData()
+    json_data, instance = book_json_data(rename_strategy, test_data)
+    config = DTOConfig(rename_strategy=rename_strategy)
+    dto = SQLAlchemyDTO[Annotated[Book, config]]
+
+    @post(dto=dto, signature_namespace={"Book": Book})
+    def post_handler(data: Book) -> Book:
+        return data
+
+    @get(dto=dto, signature_namespace={"Book": Book})
+    def get_handler() -> Book:
+        return instance
+
+    with create_test_client(
+        route_handlers=[post_handler, get_handler],
+        debug=True,
+    ) as client:
+        response_callback = client.get("/")
+        assert response_callback.json() == json_data
+
+        response_callback = client.post("/", json=json_data)
+        assert response_callback.json() == json_data
diff --git a/tests/dto/factory/test_integration.py b/tests/dto/factory/test_integration.py
--- a/tests/dto/factory/test_integration.py
+++ b/tests/dto/factory/test_integration.py
@@ -1,6 +1,8 @@
+# ruff: noqa: UP007
 from __future__ import annotations
 
 from dataclasses import dataclass, field
+from typing import Optional
 
 import pytest
 from typing_extensions import Annotated
@@ -76,11 +78,17 @@ def handler(data: Foo) -> Foo:
         assert response.json() == {"baz": "hello"}
 
 
+@dataclass
+class Spam:
+    main_id: str = "spam-id"
+
+
 @dataclass
 class Foo:
     bar: str = "hello"
     SPAM: str = "bye"
     spam_bar: str = "welcome"
+    spam_model: Optional[Spam] = None
 
 
 @pytest.mark.parametrize(
@@ -91,6 +99,7 @@ class Foo:
         (lambda x: x[::-1], Foo(bar="h", SPAM="bye!"), ["rab", "MAPS"], {"rab": "h", "MAPS": "bye!"}),
         ("camel", Foo(spam_bar="star"), ["spamBar"], {"spamBar": "star"}),
         ("pascal", Foo(spam_bar="star"), ["SpamBar"], {"SpamBar": "star"}),
+        ("camel", Foo(spam_model=Spam()), ["spamModel"], {"spamModel": {"mainId": "spam-id"}}),
     ],
 )
 def test_fields_alias_generator(
@@ -108,11 +117,6 @@ def handler(data: Foo) -> Foo:
         assert data.SPAM == instance.SPAM
         return data
 
-    with create_test_client(
-        route_handlers=[
-            handler,
-        ],
-        debug=True,
-    ) as client:
+    with create_test_client(route_handlers=[handler], debug=True) as client:
         response_callback = client.post("/", json=data)
         assert all([response_callback.json()[f] == data[f] for f in tested_fields])