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,8 +1,8 @@
 from __future__ import annotations
 
-from dataclasses import replace
+from dataclasses import asdict, replace
 from functools import singledispatchmethod
-from typing import TYPE_CHECKING, Collection, Generic, Optional, TypeVar
+from typing import TYPE_CHECKING, ClassVar, Collection, Generic, Optional, TypeVar
 
 from sqlalchemy import Column, inspect, orm, sql
 from sqlalchemy.ext.associationproxy import AssociationProxy, AssociationProxyExtensionType
@@ -21,6 +21,7 @@
 )
 
 from litestar.dto.base_dto import AbstractDTO
+from litestar.dto.config import DTOConfig, SQLAlchemyDTOConfig
 from litestar.dto.data_structures import DTOFieldDefinition
 from litestar.dto.field import DTO_FIELD_META_KEY, DTOField, Mark
 from litestar.exceptions import ImproperlyConfiguredException
@@ -44,6 +45,20 @@
 class SQLAlchemyDTO(AbstractDTO[T], Generic[T]):
     """Support for domain modelling with SQLAlchemy."""
 
+    config: ClassVar[SQLAlchemyDTOConfig]
+
+    @staticmethod
+    def _ensure_sqla_dto_config(config: DTOConfig | SQLAlchemyDTOConfig) -> SQLAlchemyDTOConfig:
+        if not isinstance(config, SQLAlchemyDTOConfig):
+            return SQLAlchemyDTOConfig(**asdict(config))
+
+        return config
+
+    def __init_subclass__(cls, **kwargs: Any) -> None:
+        super().__init_subclass__(**kwargs)
+        if hasattr(cls, "config"):
+            cls.config = cls._ensure_sqla_dto_config(cls.config)
+
     @singledispatchmethod
     @classmethod
     def handle_orm_descriptor(
@@ -187,25 +202,54 @@ def generate_field_definitions(cls, model_type: type[DeclarativeBase]) -> Genera
         namespace = {**SQLA_NS, **{m.class_.__name__: m.class_ for m in mapper.registry.mappers if m is not mapper}}
         model_type_hints = cls.get_model_type_hints(model_type, namespace=namespace)
         model_name = model_type.__name__
+        include_implicit_fields = cls.config.include_implicit_fields
 
         # the same hybrid property descriptor can be included in `all_orm_descriptors` multiple times, once
         # for each method name it is bound to. We only need to see it once, so track views of it here.
         seen_hybrid_descriptors: set[hybrid_property] = set()
-        skipped_columns: set[str] = set()
+        skipped_descriptors: set[str] = set()
         for composite_property in mapper.composites:
             for attr in composite_property.attrs:
                 if isinstance(attr, (MappedColumn, Column)):
-                    skipped_columns.add(attr.name)
+                    skipped_descriptors.add(attr.name)
                 elif isinstance(attr, str):
-                    skipped_columns.add(attr)
+                    skipped_descriptors.add(attr)
         for key, orm_descriptor in mapper.all_orm_descriptors.items():
-            if isinstance(orm_descriptor, hybrid_property):
+            if is_hybrid_property := isinstance(orm_descriptor, hybrid_property):
                 if orm_descriptor in seen_hybrid_descriptors:
                     continue
 
                 seen_hybrid_descriptors.add(orm_descriptor)
 
-            if key in skipped_columns:
+            if key in skipped_descriptors:
+                continue
+
+            should_skip_descriptor = False
+            dto_field: DTOField | None = None
+            if hasattr(orm_descriptor, "property"):
+                dto_field = orm_descriptor.property.info.get(DTO_FIELD_META_KEY)  # pyright: ignore
+
+            # Case 1
+            is_field_marked_not_private = dto_field and dto_field.mark is not Mark.PRIVATE
+
+            # Case 2
+            should_exclude_anything_implicit = not include_implicit_fields and key not in model_type_hints
+
+            # Case 3
+            should_exclude_non_hybrid_only = (
+                not is_hybrid_property and include_implicit_fields == "hybrid-only" and key not in model_type_hints
+            )
+
+            # Descriptor is marked with with either Mark.READ_ONLY or Mark.WRITE_ONLY (see Case 1):
+            # - always include it regardless of anything else.
+            # Descriptor is not marked:
+            # - It's implicit BUT config excludes anything implicit (see Case 2): exclude
+            # - It's implicit AND not hybrid BUT config includes hybdrid-only implicit descriptors (Case 3): exclude
+            should_skip_descriptor = not is_field_marked_not_private and (
+                should_exclude_anything_implicit or should_exclude_non_hybrid_only
+            )
+
+            if should_skip_descriptor:
                 continue
 
             yield from cls.handle_orm_descriptor(
diff --git a/litestar/dto/base_dto.py b/litestar/dto/base_dto.py
--- a/litestar/dto/base_dto.py
+++ b/litestar/dto/base_dto.py
@@ -268,9 +268,7 @@ def get_dto_config_from_annotated_type(field_definition: FieldDefinition) -> DTO
         Returns:
             The type and config object extracted from the annotation.
         """
-        if configs := [item for item in field_definition.metadata if isinstance(item, DTOConfig)]:
-            return configs[0]
-        return None
+        return next((item for item in field_definition.metadata if isinstance(item, DTOConfig)), None)
 
     @classmethod
     def resolve_model_type(cls, field_definition: FieldDefinition) -> FieldDefinition:
diff --git a/litestar/dto/config.py b/litestar/dto/config.py
--- a/litestar/dto/config.py
+++ b/litestar/dto/config.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from dataclasses import dataclass, field
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal
 
 from litestar.exceptions import ImproperlyConfiguredException
 
@@ -10,7 +10,10 @@
 
     from litestar.dto.types import RenameStrategy
 
-__all__ = ("DTOConfig",)
+__all__ = (
+    "DTOConfig",
+    "SQLAlchemyDTOConfig",
+)
 
 
 @dataclass(frozen=True)
@@ -62,3 +65,17 @@ def __post_init__(self) -> None:
             raise ImproperlyConfiguredException(
                 "'include' and 'exclude' are mutually exclusive options, please use one of them"
             )
+
+
+@dataclass(frozen=True)
+class SQLAlchemyDTOConfig(DTOConfig):
+    """Additional controls for the generated SQLAlchemy DTO."""
+
+    include_implicit_fields: bool | Literal["hybrid-only"] = True
+    """Fields that are implicitly mapped are included.
+
+    Turning this off will lead to exclude all fields not using ``Mapped`` annotation,
+
+    When setting this to ``hybrid-only``, all implicitly mapped fields are excluded
+    with the exception for hybrid properties.
+    """


Test patch:
diff --git a/tests/unit/test_contrib/test_sqlalchemy/test_dto_integration.py b/tests/unit/test_contrib/test_sqlalchemy/test_dto_integration.py
--- a/tests/unit/test_contrib/test_sqlalchemy/test_dto_integration.py
+++ b/tests/unit/test_contrib/test_sqlalchemy/test_dto_integration.py
@@ -4,13 +4,25 @@
 
 import pytest
 from sqlalchemy import Column, ForeignKey, Integer, String, Table
-from sqlalchemy.orm import DeclarativeBase, Mapped, composite, declared_attr, mapped_column, relationship
+from sqlalchemy.ext.hybrid import hybrid_property
+from sqlalchemy.orm import (
+    DeclarativeBase,
+    Mapped,
+    column_property,
+    composite,
+    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 import DTOConfig
+from litestar.di import Provide
+from litestar.dto import DTOConfig, DTOField, Mark
 from litestar.dto._backend import _rename_field
+from litestar.dto.config import SQLAlchemyDTOConfig
+from litestar.dto.field import DTO_FIELD_META_KEY
 from litestar.dto.types import RenameStrategy
 from litestar.testing import create_test_client
 
@@ -481,5 +493,139 @@ def post_handler(data: Model) -> Model:
     """
     )
     with create_test_client(route_handlers=[module.post_handler]) as client:
-        response = client.post("/", json={"val": "value"})
+        response = client.post("/", json={"id": 1, "val": "value"})
         assert response.json() == {"id": 1, "val": "value"}
+
+
+async def test_disable_implicitly_mapped_columns_using_annotated_notation() -> None:
+    class Base(DeclarativeBase):
+        id: Mapped[int] = mapped_column(default=int, primary_key=True)
+
+    table = Table(
+        "vertices2",
+        Base.metadata,
+        Column("id", Integer, primary_key=True),
+        Column("field", String, nullable=True),
+    )
+
+    class Model(Base):
+        __table__ = table
+        id: Mapped[int]
+
+        @hybrid_property
+        def id_multiplied(self) -> int:
+            return self.id * 10
+
+    dto_type = SQLAlchemyDTO[Annotated[Model, SQLAlchemyDTOConfig(include_implicit_fields=False)]]
+
+    @get(
+        dto=None,
+        return_dto=dto_type,
+        signature_namespace={"Model": Model},
+        dependencies={"model": Provide(lambda: Model(id=123, field="hi"), sync_to_thread=False)},
+    )
+    def post_handler(model: Model) -> Model:
+        return model
+
+    with create_test_client(route_handlers=[post_handler]) as client:
+        response = client.get(
+            "/",
+        )
+
+        json = response.json()
+        assert json.get("field") is None
+        assert json.get("id_multiplied") is None
+
+
+async def test_disable_implicitly_mapped_columns_special() -> None:
+    class Base(DeclarativeBase):
+        id: Mapped[int] = mapped_column(default=int, primary_key=True)
+
+    table = Table(
+        "vertices2",
+        Base.metadata,
+        Column("id", Integer, primary_key=True),
+        Column("field", String, nullable=True),
+    )
+
+    class Model(Base):
+        __table__ = table
+        id: Mapped[int]
+
+    class dto_type(SQLAlchemyDTO[Model]):
+        config = SQLAlchemyDTOConfig(include_implicit_fields=False)
+
+    @get(
+        dto=None,
+        return_dto=dto_type,
+        signature_namespace={"Model": Model},
+        dependencies={"model": Provide(lambda: Model(id=123, field="hi"), sync_to_thread=False)},
+    )
+    def post_handler(model: Model) -> Model:
+        return model
+
+    with create_test_client(route_handlers=[post_handler]) as client:
+        response = client.get(
+            "/",
+        )
+
+        json = response.json()
+        assert json.get("field") is None
+
+
+async def test_disable_implicitly_mapped_columns_with_hybrid_properties_and_Mark_overrides() -> None:
+    class Base(DeclarativeBase):
+        id: Mapped[int] = mapped_column(default=int, primary_key=True)
+
+    table = Table(
+        "vertices2",
+        Base.metadata,
+        Column("id", Integer, primary_key=True),
+        Column("field", String, nullable=True),
+        Column("field2", String),
+        Column("field3", String),
+        Column("field4", String),
+    )
+
+    class Model(Base):
+        __table__ = table
+        id: Mapped[int]
+        field2 = column_property(table.c.field2, info={DTO_FIELD_META_KEY: DTOField(mark=Mark.READ_ONLY)})  # type: ignore
+        field3 = column_property(table.c.field3, info={DTO_FIELD_META_KEY: DTOField(mark=Mark.WRITE_ONLY)})  # type: ignore
+        field4 = column_property(table.c.field4, info={DTO_FIELD_META_KEY: DTOField(mark=Mark.PRIVATE)})  # type: ignore
+
+        @hybrid_property
+        def id_multiplied(self) -> int:
+            return self.id * 10
+
+    dto_type = SQLAlchemyDTO[
+        Annotated[
+            Model,
+            SQLAlchemyDTOConfig(include_implicit_fields="hybrid-only"),
+        ]
+    ]
+
+    @get(
+        dto=None,
+        return_dto=dto_type,
+        signature_namespace={"Model": Model},
+        dependencies={
+            "model": Provide(
+                lambda: Model(id=12, field="hi", field2="bye2", field3="bye3", field4="bye4"), sync_to_thread=False
+            )
+        },
+    )
+    def post_handler(model: Model) -> Model:
+        return model
+
+    with create_test_client(route_handlers=[post_handler]) as client:
+        response = client.get(
+            "/",
+        )
+
+        json = response.json()
+        assert json.get("id_multiplied") == 120
+        assert json.get("field") is None
+        assert json.get("field2") is not None
+        assert json.get("field3") is not None
+        assert json.get("field4") is None