Repository: litestar-org/litestar

Issue:
Bug: `500: Two different schemas with the title app.domain.authors.Author have been defined.`
### Description

Can be reproduced  by upgrading  pg-redis-docker to alpha6.
Reproduced example included -  https://github.com/v3ss0n/starlite-pg-redis-docker/tree/bug-alpha6


### URL to code causing the issue

http://localhost:8000/schema/swagger

### MCVE

```python
https://github.com/v3ss0n/starlite-pg-redis-docker/tree/bug-alpha6
```


### Steps to reproduce

```bash
1. Upgrade To Starlite Alpha6
2. Go to http://localhost:8000/schema/swagger
3. See error
```


### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

```
first: {"properties":{"id":{"type":"string","format":"uuid","description":"Any UUID string"},"name":{"type":"string"},"dob":{"type":"string","format":"date"},"country_id":{"oneOf":[{"type":"null"},{"type":"string","format":"uuid","description":"Any UUID string"}]}},"type":"object","required":["dob","name"],"title":"app.domain.authors.Author"}
starlite-pg-redis-docker-app-1    | second: {"properties":{"created":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid","description":"Any UUID string"},"name":{"type":"string"},"dob":{"type":"string","format":"date"},"country_id":{"oneOf":[{"type":"null"},{"type":"string","format":"uuid","description":"Any UUID string"}]},"nationality":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/app.domain.authors.Author.nationality_0"}]}},"type":"object","required":["dob","name"],"title":"app.domain.authors.Author"}

if schema.title and schema.type in (OpenAPIType.OBJECT, OpenAPIType.ARRAY):
--
782 | if schema.title in schemas and hash(schemas[schema.title]) != hash(schema):
783 | raise ImproperlyConfiguredException(
784 | f"Two different schemas with the title {schema.title} have been defined.\n\n"
785 | f"first: {encode_json(schemas[schema.title].to_schema()).decode()}\n"
786 | f"second: {encode_json(schema.to_schema()).decode()}\n\n"
787 | f"To fix this issue, either rename the base classes from which these titles are derived or manually"
788 | f"set a 'title' kwarg in the route handler."
789 | )
790 | schemas[schema.title] = schema
```

### Litestar Version

litestar = { version = "==2.0.0a6" , extras = ["redis", "cli"] }


### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/1643">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/1643/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/1643/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->

Bug: `500: Two different schemas with the title app.domain.authors.Author have been defined.`
### Description

Can be reproduced  by upgrading  pg-redis-docker to alpha6.
Reproduced example included -  https://github.com/v3ss0n/starlite-pg-redis-docker/tree/bug-alpha6


### URL to code causing the issue

http://localhost:8000/schema/swagger

### MCVE

```python
https://github.com/v3ss0n/starlite-pg-redis-docker/tree/bug-alpha6
```


### Steps to reproduce

```bash
1. Upgrade To Starlite Alpha6
2. Go to http://localhost:8000/schema/swagger
3. See error
```


### Screenshots

```bash
"![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"
```


### Logs

```
first: {"properties":{"id":{"type":"string","format":"uuid","description":"Any UUID string"},"name":{"type":"string"},"dob":{"type":"string","format":"date"},"country_id":{"oneOf":[{"type":"null"},{"type":"string","format":"uuid","description":"Any UUID string"}]}},"type":"object","required":["dob","name"],"title":"app.domain.authors.Author"}
starlite-pg-redis-docker-app-1    | second: {"properties":{"created":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid","description":"Any UUID string"},"name":{"type":"string"},"dob":{"type":"string","format":"date"},"country_id":{"oneOf":[{"type":"null"},{"type":"string","format":"uuid","description":"Any UUID string"}]},"nationality":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/app.domain.authors.Author.nationality_0"}]}},"type":"object","required":["dob","name"],"title":"app.domain.authors.Author"}

if schema.title and schema.type in (OpenAPIType.OBJECT, OpenAPIType.ARRAY):
--
782 | if schema.title in schemas and hash(schemas[schema.title]) != hash(schema):
783 | raise ImproperlyConfiguredException(
784 | f"Two different schemas with the title {schema.title} have been defined.\n\n"
785 | f"first: {encode_json(schemas[schema.title].to_schema()).decode()}\n"
786 | f"second: {encode_json(schema.to_schema()).decode()}\n\n"
787 | f"To fix this issue, either rename the base classes from which these titles are derived or manually"
788 | f"set a 'title' kwarg in the route handler."
789 | )
790 | schemas[schema.title] = schema
```

### Litestar Version

litestar = { version = "==2.0.0a6" , extras = ["redis", "cli"] }


### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/1643">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/1643/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/1643/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->

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
@@ -3,6 +3,7 @@
 """
 from __future__ import annotations
 
+import secrets
 from abc import ABC, abstractmethod
 from typing import TYPE_CHECKING, Final, Generic, TypeVar, Union
 
@@ -408,6 +409,11 @@ def _create_union_type(
             has_nested=any(_determine_has_nested(t) for t in inner_types),
         )
 
+    def _gen_unique_name_id(self, unique_name: str, size: int = 12) -> str:
+        # Generate a unique ID
+        # Convert the ID to a short alphanumeric string
+        return f"{unique_name}-{secrets.token_hex(8)}"
+
 
 def _filter_exclude(exclude: AbstractSet[str], field_name: str) -> AbstractSet[str]:
     """Filter exclude set to only include exclusions for the given field name."""
diff --git a/litestar/dto/factory/_backends/msgspec/backend.py b/litestar/dto/factory/_backends/msgspec/backend.py
--- a/litestar/dto/factory/_backends/msgspec/backend.py
+++ b/litestar/dto/factory/_backends/msgspec/backend.py
@@ -26,7 +26,8 @@ class MsgspecDTOBackend(AbstractDTOBackend[Struct]):
     __slots__ = ()
 
     def create_transfer_model_type(self, unique_name: str, field_definitions: FieldDefinitionsType) -> type[Struct]:
-        return _create_struct_for_field_definitions(unique_name, field_definitions)
+        fqn_uid: str = self._gen_unique_name_id(unique_name)
+        return _create_struct_for_field_definitions(fqn_uid, field_definitions)
 
     def parse_raw(self, raw: bytes, connection_context: ConnectionContext) -> Struct | Collection[Struct]:
         return decode_media_type(  # type:ignore[no-any-return]
diff --git a/litestar/dto/factory/_backends/pydantic/backend.py b/litestar/dto/factory/_backends/pydantic/backend.py
--- a/litestar/dto/factory/_backends/pydantic/backend.py
+++ b/litestar/dto/factory/_backends/pydantic/backend.py
@@ -25,7 +25,8 @@ class PydanticDTOBackend(AbstractDTOBackend[BaseModel]):
     __slots__ = ()
 
     def create_transfer_model_type(self, unique_name: str, field_definitions: FieldDefinitionsType) -> type[BaseModel]:
-        return _create_model_for_field_definitions(unique_name, field_definitions)
+        fqn_uid: str = self._gen_unique_name_id(unique_name)
+        return _create_model_for_field_definitions(fqn_uid, field_definitions)
 
     def parse_raw(self, raw: bytes, connection_context: ConnectionContext) -> BaseModel | Collection[BaseModel]:
         return decode_media_type(  # type:ignore[no-any-return]


Test patch:
diff --git a/tests/dto/factory/backends/test_backends.py b/tests/dto/factory/backends/test_backends.py
--- a/tests/dto/factory/backends/test_backends.py
+++ b/tests/dto/factory/backends/test_backends.py
@@ -12,13 +12,14 @@
 from litestar.dto.factory import DTOConfig
 from litestar.dto.factory._backends import MsgspecDTOBackend, PydanticDTOBackend
 from litestar.dto.factory._backends.abc import BackendContext
-from litestar.dto.factory._backends.types import CollectionType, SimpleType
+from litestar.dto.factory._backends.types import CollectionType, SimpleType, TransferFieldDefinition
 from litestar.dto.factory.stdlib.dataclass import DataclassDTO
 from litestar.dto.interface import ConnectionContext
 from litestar.enums import MediaType
 from litestar.exceptions import SerializationException
 from litestar.openapi.spec.reference import Reference
 from litestar.serialization import encode_json
+from litestar.types.empty import Empty
 from litestar.utils.signature import ParsedType
 
 if TYPE_CHECKING:
@@ -175,6 +176,32 @@ def test_backend_create_openapi_schema(backend_type: type[AbstractDTOBackend], b
     assert nested_schema.properties["b"].type == "string"
 
 
+@pytest.mark.parametrize("backend_type", [MsgspecDTOBackend, PydanticDTOBackend])
+def test_backend_model_name_uniqueness(backend_type: type[AbstractDTOBackend], backend_context: BackendContext) -> None:
+    backend = backend_type(backend_context)
+    unique_names: set = set()
+    transfer_type = SimpleType(parsed_type=ParsedType(int), nested_field_info=None)
+
+    fd = (
+        TransferFieldDefinition(
+            name="a",
+            default=Empty,
+            parsed_type=ParsedType(int),
+            default_factory=None,
+            dto_field=None,
+            unique_model_name="some_module.SomeModel",
+            serialization_name="a",
+            transfer_type=transfer_type,
+            is_partial=False,
+        ),
+    )
+    for i in range(100):
+        model_class = backend.create_transfer_model_type("some_module.SomeModel", fd)
+        model_name = model_class.__name__
+        assert model_name not in unique_names
+        unique_names.add(model_name)
+
+
 @pytest.mark.parametrize("backend_type", [MsgspecDTOBackend, PydanticDTOBackend])
 def test_backend_populate_data_from_raw(
     backend_type: type[AbstractDTOBackend], backend_context: BackendContext, connection_context: ConnectionContext
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
@@ -176,3 +176,27 @@ def handler(data: DTOData[User]) -> User:
     with create_test_client(route_handlers=[handler], debug=True) as client:
         response = client.patch("/", json={"age": 41, "read_only": "whoops"})
         assert response.json() == {"name": "John", "age": 41, "read_only": "read-only"}
+
+
+def test_dto_openapi_model_name_collision() -> None:
+    @dataclass
+    class Bar:
+        id: int
+        foo: str
+
+    write_dto = DataclassDTO[Annotated[Bar, DTOConfig(exclude={"id"})]]
+    read_dto = DataclassDTO[Bar]
+
+    @post(dto=write_dto, return_dto=read_dto, signature_namespace={"Bar": Bar})
+    def handler(data: Bar) -> Bar:
+        return data
+
+    with create_test_client(route_handlers=[handler], debug=True) as client:
+        response = client.get("/schema/openapi.json")
+        schemas = list(response.json()["components"]["schemas"].values())
+        assert len(schemas) == 2
+        assert schemas[0] != schemas[1]
+        assert all(
+            k.startswith("tests.dto.factory.test_integration.test_dto_openapi_model_name_collision.<locals>.Bar")
+            for k in response.json()["components"]["schemas"]
+        )