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/repository.py b/litestar/contrib/sqlalchemy/repository.py
--- a/litestar/contrib/sqlalchemy/repository.py
+++ b/litestar/contrib/sqlalchemy/repository.py
@@ -278,7 +278,9 @@ async def get_or_create(
                 field = getattr(existing, field_name, None)
                 if field and field != new_field_value:
                     setattr(existing, field_name, new_field_value)
-            return (await self.update(existing)), False
+            existing = await self._attach_to_session(existing, strategy="merge")
+            await self.session.flush()
+            self.session.expunge(existing)
         return existing, False
 
     async def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
@@ -315,7 +317,11 @@ async def update(self, data: ModelT) -> ModelT:
             NotFoundError: If no instance found with same identifier as `data`.
         """
         with wrap_sqlalchemy_exception():
-            instance = await self._attach_to_session(data)
+            item_id = self.get_id_attribute_value(data)
+            # this will raise for not found, and will put the item in the session
+            await self.get(item_id)
+            # this will merge the inbound data to the instance we just put in the session
+            instance = await self._attach_to_session(data, strategy="merge")
             await self.session.flush()
             self.session.expunge(instance)
             return instance
@@ -806,7 +812,9 @@ def get_or_create(
                 field = getattr(existing, field_name, None)
                 if field and field != new_field_value:
                     setattr(existing, field_name, new_field_value)
-            return (self.update(existing)), False
+            existing = self._attach_to_session(existing, strategy="merge")
+            self.session.flush()
+            self.session.expunge(existing)
         return existing, False
 
     def count(self, *filters: FilterTypes, **kwargs: Any) -> int:
@@ -843,7 +851,11 @@ def update(self, data: ModelT) -> ModelT:
             NotFoundError: If no instance found with same identifier as `data`.
         """
         with wrap_sqlalchemy_exception():
-            instance = self._attach_to_session(data)
+            item_id = self.get_id_attribute_value(data)
+            # this will raise for not found, and will put the item in the session
+            self.get(item_id)
+            # this will merge the inbound data to the instance we just put in the session
+            instance = self._attach_to_session(data, strategy="merge")
             self.session.flush()
             self.session.expunge(instance)
             return instance


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -18,7 +18,7 @@ on:
 jobs:
   test:
     runs-on: ${{ inputs.os }}
-    timeout-minutes: 15
+    timeout-minutes: 20
     defaults:
       run:
         shell: bash
diff --git a/tests/contrib/sqlalchemy/repository/test_sqlalchemy_async.py b/tests/contrib/sqlalchemy/repository/test_sqlalchemy_async.py
--- a/tests/contrib/sqlalchemy/repository/test_sqlalchemy_async.py
+++ b/tests/contrib/sqlalchemy/repository/test_sqlalchemy_async.py
@@ -254,12 +254,33 @@ async def test_sqlalchemy_repo_get_or_create_member_existing(
     result_mock = MagicMock()
     result_mock.scalar_one_or_none = MagicMock(return_value=mock_instance)
     execute_mock = AsyncMock(return_value=result_mock)
+    attach_to_session_mock = AsyncMock(return_value=mock_instance)
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
+    monkeypatch.setattr(mock_repo, "_attach_to_session", attach_to_session_mock)
     instance, created = await mock_repo.get_or_create(id="instance-id")
     assert instance is mock_instance
     assert created is False
     mock_repo.session.expunge.assert_called_with(mock_instance)
-    mock_repo.session.add.assert_called_once()
+    mock_repo.session.merge.assert_not_called()
+
+
+async def test_sqlalchemy_repo_get_or_create_member_existing_upsert(
+    mock_repo: SQLAlchemyAsyncRepository, monkeypatch: MonkeyPatch
+) -> None:
+    """Test expected method calls for member get or create operation (existing)."""
+    mock_instance = MagicMock()
+    result_mock = MagicMock()
+    result_mock.scalar_one_or_none = MagicMock(return_value=mock_instance)
+    execute_mock = AsyncMock(return_value=result_mock)
+    attach_to_session_mock = AsyncMock(return_value=mock_instance)
+    monkeypatch.setattr(mock_repo, "_execute", execute_mock)
+    monkeypatch.setattr(mock_repo, "_attach_to_session", attach_to_session_mock)
+    instance, created = await mock_repo.get_or_create(id="instance-id", upsert=True, an_extra_attribute="yep")
+    assert instance is mock_instance
+    assert created is False
+    mock_repo.session.expunge.assert_called_with(mock_instance)
+    mock_repo._attach_to_session.assert_called_once()
+    mock_repo.session.flush.assert_called_once()
 
 
 async def test_sqlalchemy_repo_get_or_create_member_existing_no_upsert(
@@ -441,7 +462,7 @@ async def test_sqlalchemy_repo_update(mock_repo: SQLAlchemyAsyncRepository, monk
     mock_repo.session.merge.return_value = mock_instance
     instance = await mock_repo.update(mock_instance)
     assert instance is mock_instance
-    mock_repo.session.add.assert_called_once_with(mock_instance)
+    mock_repo.session.merge.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
     mock_repo.session.expunge.assert_called_once_with(mock_instance)
     mock_repo.session.commit.assert_not_called()
diff --git a/tests/contrib/sqlalchemy/repository/test_sqlalchemy_sync.py b/tests/contrib/sqlalchemy/repository/test_sqlalchemy_sync.py
--- a/tests/contrib/sqlalchemy/repository/test_sqlalchemy_sync.py
+++ b/tests/contrib/sqlalchemy/repository/test_sqlalchemy_sync.py
@@ -252,11 +252,33 @@ def test_sqlalchemy_repo_get_or_create_member_existing(
     result_mock.scalar_one_or_none = MagicMock(return_value=mock_instance)
     execute_mock = MagicMock(return_value=result_mock)
     monkeypatch.setattr(mock_repo, "_execute", execute_mock)
+    attach_to_session_mock = MagicMock(return_value=mock_instance)
+    monkeypatch.setattr(mock_repo, "_execute", execute_mock)
+    monkeypatch.setattr(mock_repo, "_attach_to_session", attach_to_session_mock)
     instance, created = mock_repo.get_or_create(id="instance-id")
     assert instance is mock_instance
     assert created is False
     mock_repo.session.expunge.assert_called_with(mock_instance)
-    mock_repo.session.add.assert_called_once()
+    mock_repo.session.merge.assert_not_called()
+
+
+def test_sqlalchemy_repo_get_or_create_member_existing_upsert(
+    mock_repo: SQLAlchemySyncRepository, monkeypatch: MonkeyPatch
+) -> None:
+    """Test expected method calls for member get or create operation (existing)."""
+    mock_instance = MagicMock()
+    result_mock = MagicMock()
+    result_mock.scalar_one_or_none = MagicMock(return_value=mock_instance)
+    execute_mock = MagicMock(return_value=result_mock)
+    attach_to_session_mock = MagicMock(return_value=mock_instance)
+    monkeypatch.setattr(mock_repo, "_execute", execute_mock)
+    monkeypatch.setattr(mock_repo, "_attach_to_session", attach_to_session_mock)
+    instance, created = mock_repo.get_or_create(id="instance-id", upsert=True, an_extra_attribute="yep")
+    assert instance is mock_instance
+    assert created is False
+    mock_repo.session.expunge.assert_called_with(mock_instance)
+    mock_repo._attach_to_session.assert_called_once()
+    mock_repo.session.flush.assert_called_once()
 
 
 def test_sqlalchemy_repo_get_or_create_member_existing_no_upsert(
@@ -431,10 +453,10 @@ def test_sqlalchemy_repo_update(mock_repo: SQLAlchemySyncRepository, monkeypatch
     monkeypatch.setattr(mock_repo, "get_id_attribute_value", get_id_value_mock)
     get_mock = MagicMock()
     monkeypatch.setattr(mock_repo, "get", get_mock)
-    mock_repo.session.add.return_value = mock_instance
+    mock_repo.session.merge.return_value = mock_instance
     instance = mock_repo.update(mock_instance)
     assert instance is mock_instance
-    mock_repo.session.add.assert_called_once_with(mock_instance)
+    mock_repo.session.merge.assert_called_once_with(mock_instance)
     mock_repo.session.flush.assert_called_once()
     mock_repo.session.expunge.assert_called_once_with(mock_instance)
     mock_repo.session.commit.assert_not_called()