flasgger / flasgger

Easy OpenAPI specs and Swagger UI for your Flask API
http://flasgger.pythonanywhere.com/
MIT License
3.62k stars 525 forks source link

Serving multiple flasgger pages #571

Open markchoward7 opened 1 year ago

markchoward7 commented 1 year ago

Is it possible to have multiple flasgger pages? For instance, if my API has multiple versions, I would like to have one API spec at /api/v1 and another at /api/v2.

In my attempts to get it working currently, I get the following error: ValueError: The name 'flasgger' is already registered for a different blueprint. Use 'name=' to provide a unique name.

This is roughly the code that I am trying:

v1_spec = APISpec(title="myapp", version="1", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]
v2_spec = APISpec(title="myapp", version="2", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]

with app.test_request_context():
  for rule in app.url_map.iter_rules():
    if "v1" in rule.endpoint:
      v1_spec.path(view=app.view_functions[rule.endpoint])
    if "v2" in rule.endpoint:
      v2_spec.path(view=app.view_functions[rule.endpoint])

v1_template = v1_spec.to_flasgger(app)
v2_template = v2_spec.to_flasgger(app)

v1_swagger_config = Swagger.DEFAULT_CONFIG
v1_swagger_config["specs_route"] = "/api/v1"
Swagger(app, template=v1_template, config=v1_swagger_config)

v2_swagger_config = Swagger.DEFAULT_CONFIG
v2_swagger_config["specs_route"] = "/api/v2"
Swagger(app, template=v2_template, config=v2_swagger_config)
markchoward7 commented 1 year ago

Continuing to work on this, I am getting closer to finding a working solution, although it requires an update to APIDocsView class.

I can set endpoint on the swagger_config objects to get past the original error. This causes a new error in the APIDocsView where it can't find the appropriate view because it will look for flassger.static instead of <endpoint>.static.

Updating that is a simple fix, which prevents the application from erroring out, but both hosted docs show all endpoints instead of just their respective endpoints. This is true even if I remove all code relating to the v2_spec, v2_template, and v2_swagger_config. Using the config["specs"][0]["rule_filter"] fixes it while the v2 code is still removed, but when it comes back then it is back to showing all endpoints.

For reference this is roughly the code I am trying:

v1_spec = APISpec(title="myapp", version="1", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]
v2_spec = APISpec(title="myapp", version="2", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]

with app.test_request_context():
  for rule in app.url_map.iter_rules():
    if "v1" in rule.endpoint:
      v1_spec.path(view=app.view_functions[rule.endpoint])
    if "v2" in rule.endpoint:
      v2_spec.path(view=app.view_functions[rule.endpoint])

v1_template = v1_spec.to_flasgger(app)
v2_template = v2_spec.to_flasgger(app)

v1_swagger_config = Swagger.DEFAULT_CONFIG
v1_swagger_config["specs_route"] = "/api/v1"
v1_swagger_config["endpoint"] = "flasgger-v1"
v1_swagger_config["specs"][0]["rule_filter"] = lambda rule: "v1" in rule.endpoint
Swagger(app, template=v1_template, config=v1_swagger_config)

v2_swagger_config = Swagger.DEFAULT_CONFIG
v2_swagger_config["specs_route"] = "/api/v2"
v2_swagger_config["endpoint"] = "flasgger-v2"
v2_swagger_config["specs"][0]["rule_filter"] = lambda rule: "v2" in rule.endpoint
Swagger(app, template=v2_template, config=v2_swagger_config)

And the update for APIDocsView is simply replacing any of the references to "flasgger.static" to "{0}.static".format(base_endpoint)

markchoward7 commented 1 year ago

With some more config options, it gets closer. Setting ["specs"][0]["endpoint"] uniquely for each as well as ["specs"][0]["route"] (which seems to have to start with apispec_). This appears to properly separate the endpoints, but the second Swagger call seems to overwrite the first as either endpoint I go to will only return the second's spec.

v1_spec = APISpec(title="myapp", version="1", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]
v2_spec = APISpec(title="myapp", version="2", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]

with app.test_request_context():
  for rule in app.url_map.iter_rules():
    if "v1" in rule.endpoint:
      v1_spec.path(view=app.view_functions[rule.endpoint])
    if "v2" in rule.endpoint:
      v2_spec.path(view=app.view_functions[rule.endpoint])

v1_template = v1_spec.to_flasgger(app)
v2_template = v2_spec.to_flasgger(app)

v1_swagger_config = Swagger.DEFAULT_CONFIG
v1_swagger_config["specs_route"] = "/api/v1"
v1_swagger_config["endpoint"] = "flasgger-v1"
v1_swagger_config["specs"][0]["rule_filter"] = lambda rule: "v1" in rule.endpoint
v1_swagger_config["specs"][0]["endpoint"] = "apispec_v1"
v1_swagger_config["specs"][0]["route"] = "/apispec_v1.json"
Swagger(app, template=v1_template, config=v1_swagger_config)

v2_swagger_config = Swagger.DEFAULT_CONFIG
v2_swagger_config["specs_route"] = "/api/v2"
v2_swagger_config["endpoint"] = "flasgger-v2"
v2_swagger_config["specs"][0]["rule_filter"] = lambda rule: "v2" in rule.endpoint
v2_swagger_config["specs"][0]["endpoint"] = "apispec_v2"
v2_swagger_config["specs"][0]["route"] = "/apispec_v2.json"
Swagger(app, template=v2_template, config=v2_swagger_config)
markchoward7 commented 1 year ago

After some more investigation, looks like the second Swagger instance was overriding the first's config. I tried to fix it using .copy() and dict(), but it seemed to overwrite either way. I ended up fixing it by just declaring each config as its own dictionary entirely.

The following code, along with my changes to APIDocsView (PR incoming) works:

v1_spec = APISpec(title="myapp", version="1", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]
v2_spec = APISpec(title="myapp", version="2", openapi_version="3.0.2", plugins=[FlaskPlugin(), MarshmallowPlugin()]

with app.test_request_context():
  for rule in app.url_map.iter_rules():
    if "v1" in rule.endpoint:
      v1_spec.path(view=app.view_functions[rule.endpoint])
    if "v2" in rule.endpoint:
      v2_spec.path(view=app.view_functions[rule.endpoint])

v1_template = v1_spec.to_flasgger(app)
v2_template = v2_spec.to_flasgger(app)

v2_swagger_config = {
  "headers": [],
  "specs_route": "/api/v1",
  "endpoint": "flasgger-v1",
  "specs": [
    {
      "endpoint": "apispec_v1",
      "route": "/apispec_v1.json",
      "rule_filter": lambda rule: "v1" in rule.endpoint,
      "model_filter": lambda tag: True,
    }
  ],
}
Swagger(app, template=v1_template, config=v1_swagger_config)

v2_swagger_config = {
  "headers": [],
  "specs_route": "/api/v2",
  "endpoint": "flasgger-v2",
  "specs": [
    {
      "endpoint": "apispec_v2",
      "route": "/apispec_v2.json",
      "rule_filter": lambda rule: "v2" in rule.endpoint,
      "model_filter": lambda tag: True,
    }
  ],
}
Swagger(app, template=v2_template, config=v2_swagger_config)