Neoteroi / BlackSheep

Fast ASGI web framework for Python
https://www.neoteroi.dev/blacksheep/
MIT License
1.8k stars 75 forks source link

FR: custom cdn options #423

Closed joshua-auchincloss closed 7 months ago

joshua-auchincloss commented 8 months ago

Hello,

Hoping to add the following features, rationale to enable support for e.g. private resource CDNs:

codecov-commenter commented 7 months ago

Codecov Report

Attention: 1 lines in your changes are missing coverage. Please review.

Comparison is base (310dc28) 97.70% compared to head (7bf3178) 97.69%.

Files Patch % Lines
blacksheep/server/openapi/ui.py 96.55% 1 Missing :warning:

:exclamation: Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #423 +/- ## ========================================== - Coverage 97.70% 97.69% -0.01% ========================================== Files 67 67 Lines 6277 6300 +23 ========================================== + Hits 6133 6155 +22 - Misses 144 145 +1 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

RobertoPrevato commented 7 months ago

Hi @joshua-auchincloss I apologize for replying so late. I considered merging your contribution and also made some changes to your code. But I finally changed mind.

With the current code API I implemented, all it takes to achieve what you want is to define a subclass of the provided classes and including the desired HTML, like in this example:

from dataclasses import dataclass
from pathlib import Path

from blacksheep import Application
from blacksheep.server.openapi.v3 import OpenAPIHandler
from blacksheep.server.openapi.ui import SwaggerUIProvider, UIOptions
from openapidocs.v3 import Info

app = Application()

class CustomUIProvider(SwaggerUIProvider):
    def get_openapi_ui_html(self, options: UIOptions) -> str:
        _template = Path("example.html").read_text()
        return _template.replace("{options.spec_url}", options.spec_url)

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
# Set the UI provider as desired:
docs.ui_providers = [CustomUIProvider()]
docs.bind_app(app)

@dataclass
class Foo:
    foo: str

@app.route("/foo")
async def get_foo() -> Foo:
    return Foo("Hello!")

example.html:

<!DOCTYPE html>
<html>
<head>
    <title>My desired title</title>
    <link rel="icon" href="/favicon.png"/>
    <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui.css">
</head>
<body>
    <div id="swagger-ui"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui-bundle.min.js"></script>
    <script>
    const ui = SwaggerUIBundle({
        url: '{options.spec_url}',
        oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',
        dom_id: '#swagger-ui',
        presets: [
            SwaggerUIBundle.presets.apis,
            SwaggerUIBundle.SwaggerUIStandalonePreset
        ],
        layout: "BaseLayout",
        deepLinking: true,
        showExtensions: true,
        showCommonExtensions: true
    })
    </script>
</body>
</html>

It is just 6 lines of Python, and a simple HTML file:

+from blacksheep.server.openapi.ui import SwaggerUIProvider, UIOptions
from openapidocs.v3 import Info

app = Application()

+class CustomUIProvider(SwaggerUIProvider):
+    def get_openapi_ui_html(self, options: UIOptions) -> str:
+        _template = Path("example.html").read_text()
+        return _template.replace("{options.spec_url}", options.spec_url)

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))
# Set the UI provider as desired:
+docs.ui_providers = [CustomUIProvider()]
docs.bind_app(app)

With the additional benefit that you can control everything of the HTML (favicon, metatags, etc.), not only the URL sources for the static files. I don't think adding support for controlling the URL sources of static files is a better alternative.

joshua-auchincloss commented 7 months ago

Hi @joshua-auchincloss

I apologize for replying so late. I considered merging your contribution and also made some changes to your code. But I finally changed mind.

With the current code API I implemented, all it takes to achieve what you want is to define a subclass of the provided classes and including the desired HTML, like in this example:


from dataclasses import dataclass

from pathlib import Path

from blacksheep import Application

from blacksheep.server.openapi.v3 import OpenAPIHandler

from blacksheep.server.openapi.ui import SwaggerUIProvider, UIOptions

from openapidocs.v3 import Info

app = Application()

class CustomUIProvider(SwaggerUIProvider):

    def get_openapi_ui_html(self, options: UIOptions) -> str:

        _template = Path("example.html").read_text()

        return _template.replace("{options.spec_url}", options.spec_url)

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))

# Set the UI provider as desired:

docs.ui_providers = [CustomUIProvider()]

docs.bind_app(app)

@dataclass

class Foo:

    foo: str

@app.route("/foo")

async def get_foo() -> Foo:

    return Foo("Hello!")

example.html:


<!DOCTYPE html>

<html>

<head>

    <title>My desired title</title>

    <link rel="icon" href="/favicon.png"/>

    <link type="text/css" rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui.css">

</head>

<body>

    <div id="swagger-ui"></div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.10.0/swagger-ui-bundle.min.js"></script>

    <script>

    const ui = SwaggerUIBundle({

        url: '{options.spec_url}',

        oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',

        dom_id: '#swagger-ui',

        presets: [

            SwaggerUIBundle.presets.apis,

            SwaggerUIBundle.SwaggerUIStandalonePreset

        ],

        layout: "BaseLayout",

        deepLinking: true,

        showExtensions: true,

        showCommonExtensions: true

    })

    </script>

</body>

</html>

It is just 6 lines of Python, and a simple HTML file:


+from blacksheep.server.openapi.ui import SwaggerUIProvider, UIOptions

from openapidocs.v3 import Info

app = Application()

+class CustomUIProvider(SwaggerUIProvider):

+    def get_openapi_ui_html(self, options: UIOptions) -> str:

+        _template = Path("example.html").read_text()

+        return _template.replace("{options.spec_url}", options.spec_url)

docs = OpenAPIHandler(info=Info(title="Example API", version="0.0.1"))

# Set the UI provider as desired:

+docs.ui_providers = [CustomUIProvider()]

docs.bind_app(app)

With the additional benefit that you can control everything of the HTML (favicon, metatags, etc.), not only the URL sources for the static files.

I don't think adding support for controlling the URL sources of static files is a better alternative.

Hey thanks for the reply - I appreciate the review, although our opinions differ on this I guess. From an end user perspective, why would I make a custom html handler and custom HTML source files when it's already bundled with the server in package resources? I don't think it's (unreasonable) to have support for these use cases without an extra ~2 files in my source code

RobertoPrevato commented 6 months ago

@joshua-auchincloss fair enough. I agree it's generally good to offer one more option to users. I recreated a PR from your branch. Weeks ago I modified the naming, to not assume that a custom URL always goes to a CDN (static files could also be self-hosted in the app itself, or hosted in other services that are not a content delivery network).

docs = OpenAPIHandler(info=Info(title="Cats API", version="0.0.1"))
docs.ui_providers[0].ui_files = UIFilesOptions(
    js_url=get_test_files_url("swag-js"),
    css_url=get_test_files_url("swag-css"),
)
docs.ui_providers.append(
    ReDocUIProvider(
        ui_files=UIFilesOptions(
            js_url=get_test_files_url("redoc-js"),
            fonts_url=get_test_files_url("redoc-fonts"),
        )
    )
)