Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flask-smorest with subdomains and redoc #661

Open
ca-wobeng opened this issue Jun 30, 2024 · 2 comments
Open

flask-smorest with subdomains and redoc #661

ca-wobeng opened this issue Jun 30, 2024 · 2 comments

Comments

@ca-wobeng
Copy link

ca-wobeng commented Jun 30, 2024

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://account.example.com:5000/info works
  • http://manage.example.com:5000/settings works

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
Copy link
Author

ca-wobeng commented Jul 1, 2024

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 #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
Copy link
Author

tagging @lafrech and @revmischa on thoughts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant