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/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,5 +1,13 @@
 import importlib.metadata
 import os
+from functools import partial
+from typing import Any
+
+from sphinx.addnodes import document
+from sphinx.application import Sphinx
+
+__all__ = ["setup", "update_html_context"]
+
 
 project = "Starlite"
 copyright = "2023, Starlite-API"
@@ -18,7 +26,6 @@
     "sphinxcontrib.mermaid",
 ]
 
-templates_path = ["_templates"]
 exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
 
 
@@ -120,53 +127,48 @@
     "ref.python",  # TODO: remove when https://github.com/sphinx-doc/sphinx/issues/4961 is fixed
 ]
 
-html_theme = "pydata_sphinx_theme"
+html_theme = "starlite_sphinx_theme"
 html_static_path = ["_static"]
-html_css_files = ["style.css"]
 html_js_files = ["versioning.js"]
-html_favicon = "images/favicon.ico"
-html_logo = "images/logo.svg"
+html_css_files = ["style.css"]
 html_show_sourcelink = False
-html_sidebars = {"about/*": []}
 html_title = "Starlite Framework"
 
-html_additional_pages = {"index": "landing-page.html"}
-
 
 html_theme_options = {
-    "use_edit_page_button": False,
-    "show_toc_level": 4,
-    "navbar_align": "left",
-    "icon_links": [
-        {
-            "name": "GitHub",
-            "url": "https://github.com/starlite-api/starlite",
-            "icon": "fa-brands fa-github",
-            "type": "fontawesome",
-        },
-        {
-            "name": "Discord",
-            "url": "https://discord.gg/X3FJqy8d2j",
-            "icon": "fa-brands fa-discord",
-            "type": "fontawesome",
-        },
-    ],
-    "navbar_end": ["navbar-icon-links"],
-    "navbar_persistent": ["search-button", "theme-switcher"],
-}
-
-
-html_context = {
-    "navbar_items": {
-        "Documentation": "lib/index",
+    "use_page_nav": False,
+    "github_repo_name": "starlite",
+    "logo": {
+        "link": "https://starliteproject.dev",
+    },
+    "extra_navbar_items": {
+        "Documentation": "index",
         "Community": {
-            "Contribution guide": "community/contribution-guide",
+            "Contribution Guide": "contribution-guide",
             "Code of Conduct": "https://github.com/starlite-api/.github/blob/main/CODE_OF_CONDUCT.md",
         },
         "About": {
-            "Organization": "about/organization",
-            "Releases": "about/starlite-releases",
+            "Organization": "https://starliteproject.dev/about/organization",
+            "Releases": "https://starliteproject.dev/about/starlite-releases",
         },
-        "Release notes": "release-notes/index",
-    }
+        "Release notes": {
+            "2.0 migration guide": "release-notes/migration_guide_2.html",
+            "2.x Changelog": "https://docs.starliteproject.dev/2/release-notes/changelog.html",
+            "1.x Changelog": "https://docs.starliteproject.dev/1/changelog.html",
+        },
+    },
 }
+
+
+def update_html_context(
+    app: Sphinx, pagename: str, templatename: str, context: dict[str, Any], doctree: document
+) -> None:
+    context["generate_toctree_html"] = partial(context["generate_toctree_html"], startdepth=0)
+
+
+def setup(app: Sphinx) -> dict[str, bool]:
+    app.setup_extension("starlite_sphinx_theme")
+    app.setup_extension("pydata_sphinx_theme")
+    app.connect("html-page-context", update_html_context)
+
+    return {"parallel_read_safe": True, "parallel_write_safe": True}
diff --git a/tools/build_docs.py b/tools/build_docs.py
new file mode 100644
--- /dev/null
+++ b/tools/build_docs.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+
+import argparse
+import importlib.metadata
+import json
+import os
+import shutil
+import subprocess
+from contextlib import contextmanager
+from pathlib import Path
+from typing import TypedDict
+
+REDIRECT_TEMPLATE = """
+<!DOCTYPE HTML>
+<html lang="en-US">
+    <head>
+        <title>Page Redirection</title>
+        <meta charset="UTF-8">
+        <meta http-equiv="refresh" content="0; url={target}">
+        <script type="text/javascript">window.location.href = "{target}"</script>
+    </head>
+    <body>
+        You are being redirected. If this does not work, click <a href='{target}'>this link</a>
+    </body>
+</html>
+"""
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--version", required=False)
+parser.add_argument("output")
+
+
+class VersionSpec(TypedDict):
+    versions: list[str]
+    latest: str
+
+
+@contextmanager
+def checkout(branch: str) -> None:
+    subprocess.run(["git", "checkout", branch], check=True)
+    yield
+    subprocess.run(["git", "checkout", "-"], check=True)
+
+
+def load_version_spec() -> VersionSpec:
+    versions_file = Path("docs/_static/versions.json")
+    if versions_file.exists():
+        return json.loads(versions_file.read_text())
+    return {"versions": [], "latest": ""}
+
+
+def build(output_dir: str, version: str | None) -> None:
+    if version is None:
+        version = importlib.metadata.version("starlite").rsplit(".")[0]
+    else:
+        os.environ["_STARLITE_DOCS_BUILD_VERSION"] = version
+
+    subprocess.run(["make", "docs"], check=True)
+
+    output_dir = Path(output_dir)
+    output_dir.mkdir()
+    output_dir.joinpath(".nojekyll").touch(exist_ok=True)
+
+    version_spec = load_version_spec()
+    is_latest = version == version_spec["latest"]
+
+    docs_src_path = Path("docs/_build/html")
+
+    output_dir.joinpath("index.html").write_text(REDIRECT_TEMPLATE.format(target="latest"))
+
+    if is_latest:
+        shutil.copytree(docs_src_path, output_dir / "latest", dirs_exist_ok=True)
+    shutil.copytree(docs_src_path, output_dir / version, dirs_exist_ok=True)
+
+    # copy existing versions into our output dir to preserve them when cleaning the branch
+    with checkout("gh-pages"):
+        for other_version in [*version_spec["versions"], "latest"]:
+            other_version_path = Path(other_version)
+            other_version_target_path = output_dir / other_version
+            if other_version_path.exists() and not other_version_target_path.exists():
+                shutil.copytree(other_version_path, other_version_target_path)
+
+
+def main() -> None:
+    args = parser.parse_args()
+    build(output_dir=args.output, version=args.version)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tools/publish_docs.py b/tools/publish_docs.py
deleted file mode 100644
--- a/tools/publish_docs.py
+++ /dev/null
@@ -1,105 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import importlib.metadata
-import json
-import os
-import shutil
-import subprocess
-from contextlib import contextmanager
-from pathlib import Path
-from typing import TypedDict
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--version", required=False)
-parser.add_argument("--push", action="store_true")
-
-
-class VersionSpec(TypedDict):
-    versions: list[str]
-    latest: str
-    docs_latest: str
-
-
-@contextmanager
-def checkout(branch: str) -> None:
-    subprocess.run(["git", "checkout", branch], check=True)
-    yield
-    subprocess.run(["git", "checkout", "-"], check=True)
-
-
-def add_to_versions_file(version: str) -> VersionSpec:
-    versions_file = Path("versions.json")
-    version_spec: VersionSpec
-    if versions_file.exists():
-        version_spec = json.loads(versions_file.read_text())
-    else:
-        version_spec = {"versions": [], "latest": "", "docs_latest": ""}
-
-    if version not in version_spec["versions"]:
-        version_spec["versions"].append(version)
-
-    versions_file.write_text(json.dumps(version_spec))
-
-    return version_spec
-
-
-def make_version(version: str | None, push: bool) -> None:
-    if version is None:
-        version = importlib.metadata.version("starlite").rsplit(".")[0]
-    else:
-        os.environ["_STARLITE_DOCS_BUILD_VERSION"] = version
-
-    git_add = [".nojekyll", "versions.json", version]
-    subprocess.run(["make", "docs"], check=True)
-
-    with checkout("gh-pages"):
-        Path(".nojekyll").touch(exist_ok=True)
-
-        version_spec = add_to_versions_file(version)
-        rebuild_page = version_spec["docs_latest"] == version
-        is_latest = version == version_spec["latest"]
-
-        docs_src_path = Path("docs/_build/html")
-
-        inventory_file = docs_src_path / "objects.inv"
-        shutil.copytree(docs_src_path / "lib", version, dirs_exist_ok=True)
-        shutil.copy2(inventory_file, version)
-        git_add.append(f"{version}/objects.inv")
-
-        if rebuild_page:
-            for path in docs_src_path.iterdir():
-                git_add.append(path.name)
-                if path.is_dir():
-                    if path == docs_src_path / "lib" and not is_latest:
-                        continue
-                    shutil.copytree(path, path.name, dirs_exist_ok=True)
-                else:
-                    shutil.copy2(path, ".")
-
-        if is_latest:
-            shutil.copytree(docs_src_path / "lib", "lib", dirs_exist_ok=True)
-            shutil.copy2(inventory_file, "lib")
-            git_add.append("lib/objects.inv")
-
-        shutil.rmtree("docs/_build")
-
-        for file in git_add:
-            subprocess.run(["git", "add", file], check=True)
-
-        subprocess.run(
-            ["git", "commit", "-m", f"Automatic docs build for version {version!r}", "--no-verify"],
-            check=True,
-        )
-
-        if push:
-            subprocess.run(["git", "push"], check=True)
-
-
-def main() -> None:
-    args = parser.parse_args()
-    make_version(version=args.version, push=args.push)
-
-
-if __name__ == "__main__":
-    main()


Test patch:
diff --git a/docs/lib/reference/testing.rst b/docs/reference/testing.rst
similarity index 100%
rename from docs/lib/reference/testing.rst
rename to docs/reference/testing.rst
diff --git a/docs/lib/usage/testing.rst b/docs/usage/testing.rst
similarity index 98%
rename from docs/lib/usage/testing.rst
rename to docs/usage/testing.rst
--- a/docs/lib/usage/testing.rst
+++ b/docs/usage/testing.rst
@@ -132,7 +132,7 @@ We would then be able to rewrite our test like so:
 Using sessions
 ++++++++++++++
 
-If you are using :ref:`session middleware <lib/usage/middleware/builtin-middleware:session middleware>` for session persistence
+If you are using :ref:`session middleware <usage/middleware/builtin-middleware:session middleware>` for session persistence
 across requests, then you might want to inject or inspect session data outside a request. For this,
 :class:`TestClient <.testing.TestClient>` provides two methods:
 
@@ -246,7 +246,7 @@ Another helper is the :class:`RequestFactory <starlite.testing.RequestFactory>`
 you need to test logic that expects to receive a request object.
 
 For example, lets say we wanted to unit test a *guard* function in isolation, to which end we'll reuse the examples
-from the :doc:`route guards </lib/usage/security/guards>` documentation:
+from the :doc:`route guards </usage/security/guards>` documentation:
 
 
 .. code-block:: python