marshmallow-code / flask-smorest

DB agnostic framework to build auto-documented REST APIs with Flask and marshmallow
https://flask-smorest.readthedocs.io
MIT License
633 stars 72 forks source link

flask-smorest with subdomains and redoc #661

Open ca-wobeng opened 6 days ago

ca-wobeng commented 6 days ago

I have this sample app

from flask import Flask, jsonify
from flask_smorest import Api, Blueprint as SmorestBlueprint

# Initialize Flask app
app = Flask(__name__, subdomain_matching=True)
app.url_map.default_subdomain = "docs"
app.config["API_TITLE"] = "My API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_REDOC_PATH"] = "/redoc"
app.config[
    "OPENAPI_REDOC_URL"
] = "https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"
app.config[
    "SERVER_NAME"
] = "example.com:5000"  # Configure the server name to handle subdomains

# Initialize API
api = Api(app)

# Create 'account' and 'manage' blueprint
account_blp = SmorestBlueprint("account", "account")
manage_blp = SmorestBlueprint("manage", "manage")

@account_blp.route("/info", subdomain="account")
def account_info():
    return jsonify({"message": "Account Info"})

@manage_blp.route("/settings", subdomain="manage")
def manage_settings():
    return jsonify({"message": "Manage Settings"})

# Set up Redoc for each subdomain
redoc_html = """
<!DOCTYPE html>
<html>
  <head>
    <title>Redoc</title>
    <!-- needed for adaptive design -->
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700">
    <style>
      body {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <redoc spec-url='/openapi.json'></redoc>
    <script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
  </body>
</html>
"""

# Register blueprints with API
api.register_blueprint(account_blp, subdomain="account")
api.register_blueprint(manage_blp, subdomain="manage")

# print routes to see whats registered
for rule in app.url_map.iter_rules():
    methods = ",".join(rule.methods)
    print(f"{rule.endpoint:25s} {methods:20s} {rule.rule}")

if __name__ == "__main__":
    # Run the app on all interfaces to accept requests from subdomains
    app.run(debug=True, host="0.0.0.0", port=5000)

I added the entry below in /etc/hosts

127.0.0.1 account.example.com
127.0.0.1 manage.example.com
127.0.0.1 docs.example.com

http://docs.example.com:5000/redoc works but server URL for each endpoint is missing. Both endpoints starts with http://docs.example.com:5000

ca-wobeng commented 5 days ago

was able to get it working by monkey-patching with code below but I think this issue falls in the same line of #56 and https://github.com/marshmallow-code/flask-smorest/issues/55

from flask import request, Flask, jsonify
from copy import deepcopy
from flask_smorest.utils import deepupdate

def register_blueprint(self, blp, *, parameters=None, **options):
    """Register a blueprint in the application

    Also registers documentation for the blueprint/resource

    :param Blueprint blp: Blueprint to register
    :param list parameters: List of parameter descriptions for the path parameters
        in the ``url_prefix`` of the Blueprint. Only used to document the resource.
    :param options: Keyword arguments overriding
        :class:`Blueprint <flask.Blueprint>` defaults

    Must be called after app is initialized.
    """
    blp_name = options.get("name", blp.name)
    blp_subdomain = options.get("subdomain", blp.subdomain)

    self._app.extensions["flask-smorest"]["blp_name_to_api"][blp_name] = self

    self._app.register_blueprint(blp, **options)

    # Register views in API documentation for this resource
    blp.register_views_in_doc(
        self,
        self._app,
        self.spec,
        name=blp_name,
        parameters=parameters,
        subdomain=blp_subdomain,
    )

    # Add tag relative to this resource to the global tag list
    self.spec.tag({"name": blp_name, "description": blp.description})

def register_views_in_doc(self, api, app, spec, *, name, parameters, subdomain):
    """Register views information in documentation

    If a schema in a parameter or a response appears in the spec
    `schemas` section, it is replaced by a reference in the parameter or
    response documentation:

    "schema":{"$ref": "#/components/schemas/MySchema"}
    """
    url_prefix_parameters = parameters or []

    # This method uses the documentation information associated with each
    # endpoint in self._docs to provide documentation for corresponding
    # route to the spec object.
    # Deepcopy to avoid mutating the source. Allows registering blueprint
    # multiple times (e.g. when creating multiple apps during tests).
    for endpoint, endpoint_doc_info in deepcopy(self._docs).items():
        endpoint_route_parameters = endpoint_doc_info.pop("parameters") or []
        endpoint_parameters = url_prefix_parameters + endpoint_route_parameters
        doc = {}
        # Use doc info stored by decorators to generate doc
        for method_l, operation_doc_info in endpoint_doc_info.items():
            tags = operation_doc_info.pop("tags")
            operation_doc = {}
            for func in self._prepare_doc_cbks:
                operation_doc = func(
                    operation_doc,
                    operation_doc_info,
                    api=api,
                    app=app,
                    spec=spec,
                    method=method_l,
                )
            operation_doc.update(operation_doc_info["docstring"])
            # Tag all operations with Blueprint name unless tags specified
            operation_doc["tags"] = (
                tags
                if tags is not None
                else [
                    name,
                ]
            )
            # Complete doc with manual doc info
            manual_doc = operation_doc_info.get("manual_doc", {})
            # Add servers information dynamically
            if subdomain:
                with app.app_context():
                    with app.test_request_context():
                        server_url = f"//{subdomain}.{request.host}"
                        operation_doc["servers"] = [
                            {
                                "url": server_url,
                            }
                        ]
            doc[method_l] = deepupdate(operation_doc, manual_doc)

        # Thanks to self.route, there can only be one rule per endpoint
        full_endpoint = ".".join((name, endpoint))
        rule = next(app.url_map.iter_rules(full_endpoint))

        spec.path(rule=rule, operations=doc, parameters=endpoint_parameters)

import flask_smorest.blueprint

flask_smorest.Api.register_blueprint = register_blueprint
flask_smorest.blueprint.Blueprint.register_views_in_doc = register_views_in_doc

from flask_smorest import Api, Blueprint as SmorestBlueprint

# Initialize Flask app
app = Flask(__name__, subdomain_matching=True)
app.url_map.default_subdomain = "docs"
app.config["API_TITLE"] = "My API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_REDOC_PATH"] = "/redoc"
app.config[
    "OPENAPI_REDOC_URL"
] = "https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"
app.config[
    "SERVER_NAME"
] = "example.com:5000"  # Configure the server name to handle subdomains

# Initialize API
api = Api(app)

# Create 'account' and 'manage' blueprint
account_blp = SmorestBlueprint("account", "account", subdomain="account")
manage_blp = SmorestBlueprint("manage", "manage", subdomain="manage")

@account_blp.route("/info", subdomain="account")
def account_info():
    return jsonify({"message": "Account Info"})

@manage_blp.route("/settings", subdomain="manage")
def manage_settings():
    return jsonify({"message": "Manage Settings"})

# Register blueprints with API
api.register_blueprint(account_blp, subdomain="account")
api.register_blueprint(manage_blp, subdomain="manage")

if __name__ == "__main__":
    # Run the app on all interfaces to accept requests from subdomains
    app.run(debug=True, host="0.0.0.0", port=5000)
ca-wobeng commented 5 days ago

tagging @lafrech and @revmischa on thoughts