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/repository/abc.py b/litestar/contrib/repository/abc.py
--- a/litestar/contrib/repository/abc.py
+++ b/litestar/contrib/repository/abc.py
@@ -258,7 +258,7 @@ def check_not_found(item_or_none: T | None) -> T:
         return item_or_none
 
     @classmethod
-    def get_id_attribute_value(cls, item: T) -> Any:
+    def get_id_attribute_value(cls, item: T | type[T]) -> Any:
         """Get value of attribute named as :attr:`id_attribute <AbstractRepository.id_attribute>` on ``item``.
 
         Args:
diff --git a/litestar/contrib/sqlalchemy/base.py b/litestar/contrib/sqlalchemy/base.py
--- a/litestar/contrib/sqlalchemy/base.py
+++ b/litestar/contrib/sqlalchemy/base.py
@@ -3,7 +3,7 @@
 
 import re
 from datetime import datetime
-from typing import Any, TypeVar
+from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar, runtime_checkable
 from uuid import UUID, uuid4
 
 from pydantic import AnyHttpUrl, AnyUrl, EmailStr
@@ -19,6 +19,9 @@
     registry,
 )
 
+if TYPE_CHECKING:
+    from sqlalchemy.sql import FromClause
+
 __all__ = ("AuditBase", "AuditColumns", "Base", "CommonTableAttributes", "UUIDPrimaryKey", "touch_updated_timestamp")
 
 
@@ -51,6 +54,22 @@ def touch_updated_timestamp(session: Session, *_: Any) -> None:
             instance.updated = datetime.now()  # noqa: DTZ005
 
 
+@runtime_checkable
+class ModelProtocol(Protocol):
+    """The base SQLAlchemy model protocol."""
+
+    __table__: FromClause
+    __name__: ClassVar[str]
+
+    def to_dict(self, exclude: set[str] | None = None) -> dict[str, Any]:
+        """Convert model to dictionary.
+
+        Returns:
+            dict[str, Any]: A dict representation of the model
+        """
+        ...
+
+
 @declarative_mixin
 class UUIDPrimaryKey:
     """UUID Primary Key Field Mixin."""
@@ -78,8 +97,8 @@ class CommonTableAttributes:
     """Common attributes for SQLALchemy tables."""
 
     __abstract__ = True
-    __name__: str
-    __table__: Any
+    __name__: ClassVar[str]
+    __table__: FromClause
 
     # noinspection PyMethodParameters
     @declared_attr.directive
diff --git a/litestar/contrib/sqlalchemy/repository.py b/litestar/contrib/sqlalchemy/repository.py
--- a/litestar/contrib/sqlalchemy/repository.py
+++ b/litestar/contrib/sqlalchemy/repository.py
@@ -32,7 +32,7 @@
 )
 
 T = TypeVar("T")
-ModelT = TypeVar("ModelT", bound="base.Base | base.AuditBase")
+ModelT = TypeVar("ModelT", bound="base.ModelProtocol")
 SQLARepoT = TypeVar("SQLARepoT", bound="SQLAlchemyRepository")
 SelectT = TypeVar("SelectT", bound="Select[Any]")
 RowT = TypeVar("RowT", bound=Tuple[Any, ...])
@@ -249,7 +249,7 @@ async def get_or_create(self, **kwargs: Any) -> tuple[ModelT, bool]:
         existing = await self.get_one_or_none(**kwargs)
         if existing:
             return existing, False
-        return await self.add(self.model_type(**kwargs)), True  # type: ignore[arg-type]
+        return await self.add(self.model_type(**kwargs)), True
 
     async def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
         """Get the count of records returned by a query.
@@ -263,9 +263,7 @@ async def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
         """
         statement = kwargs.pop("base_select", self.statement)
         statement = statement.with_only_columns(
-            sql_func.count(
-                self.model_type.id,
-            ),
+            sql_func.count(self.get_id_attribute_value(self.model_type)),
             maintain_column_froms=True,
         ).order_by(None)
         statement = self._apply_filters(*filters, apply_pagination=False, statement=statement)
@@ -351,9 +349,7 @@ async def list_and_count(
         statement = kwargs.pop("base_select", self.statement)
         statement = statement.add_columns(
             over(
-                sql_func.count(
-                    self.model_type.id,
-                ),
+                sql_func.count(self.get_id_attribute_value(self.model_type)),
             )
         )
         statement = self._apply_filters(*filters, statement=statement)


Test patch: