From e43835e92aed8a74f3366147401b33dd79a7473d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sat, 3 Feb 2024 11:20:00 +0100 Subject: [PATCH] Support using httpbin without flasgger Make the dependency on flasgger optional. The dependency has been added relatively recently (i.e. before the original package was abandoned but after its last release), and it is only used to provide a more dynamic landing page. This is unnecessary for use of httpbin for testing, and it introduces an indirect dependency on Rust that is problematic. With this change, flasgger is no longer installed by default. It can be enabled via "[flasgger]" extra. When flasgger is not available, httpbin redirects to the "legacy" index page. --- httpbin/core.py | 157 +++++++++++++++++++++++------------------- pyproject.toml | 4 +- tests/test_httpbin.py | 4 +- 3 files changed, 91 insertions(+), 74 deletions(-) diff --git a/httpbin/core.py b/httpbin/core.py index a82c1b88..4e7f0264 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -33,7 +33,10 @@ except ImportError: # werkzeug < 2.1 from werkzeug.wrappers import BaseResponse as Response -from flasgger import Swagger, NO_SANITIZER +try: + from flasgger import Swagger, NO_SANITIZER +except ImportError: + Swagger = None from . import filters from .helpers import ( @@ -93,79 +96,82 @@ def jsonify(*args, **kwargs): app.add_template_global("HTTPBIN_TRACKING" in os.environ, name="tracking_enabled") -app.config["SWAGGER"] = {"title": "httpbin.org", "uiversion": 3} - -template = { - "swagger": "2.0", - "info": { - "title": "httpbin.org", - "description": ( - "A simple HTTP Request & Response Service." - "
A Kenneth Reitz project." - "

Run locally:
" - "$ docker pull ghcr.io/psf/httpbin
" - "$ docker run -p 80:8080 ghcr.io/psf/httpbin" - ), - "contact": { - "responsibleOrganization": "Python Software Foundation", - "responsibleDeveloper": "Kenneth Reitz", - "url": "https://github.com/psf/httpbin/", - }, - # "termsOfService": "http://me.com/terms", - "version": version, - }, - "host": "httpbin.org", # overrides localhost:5000 - "basePath": "/", # base bash for blueprint registration - "schemes": ["https"], - "protocol": "https", - "tags": [ - { - "name": "HTTP Methods", - "description": "Testing different HTTP verbs", - # 'externalDocs': {'description': 'Learn more', 'url': 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html'} - }, - {"name": "Auth", "description": "Auth methods"}, - { - "name": "Status codes", - "description": "Generates responses with given status code", - }, - {"name": "Request inspection", "description": "Inspect the request data"}, - { - "name": "Response inspection", - "description": "Inspect the response data like caching and headers", - }, - { - "name": "Response formats", - "description": "Returns responses in different data formats", - }, - {"name": "Dynamic data", "description": "Generates random and dynamic data"}, - {"name": "Cookies", "description": "Creates, reads and deletes Cookies"}, - {"name": "Images", "description": "Returns different image formats"}, - {"name": "Redirects", "description": "Returns different redirect responses"}, - { - "name": "Anything", - "description": "Returns anything that is passed to request", +if Swagger is not None: + app.config["SWAGGER"] = {"title": "httpbin.org", "uiversion": 3} + + template = { + "swagger": "2.0", + "info": { + "title": "httpbin.org", + "description": ( + "A simple HTTP Request & Response Service." + "
A Kenneth Reitz project." + "

Run locally:
" + "$ docker pull ghcr.io/psf/httpbin
" + "$ docker run -p 80:8080 ghcr.io/psf/httpbin" + ), + "contact": { + "responsibleOrganization": "Python Software Foundation", + "responsibleDeveloper": "Kenneth Reitz", + "url": "https://github.com/psf/httpbin/", + }, + # "termsOfService": "http://me.com/terms", + "version": version, }, - ], -} - -swagger_config = { - "headers": [], - "specs": [ - { - "endpoint": "spec", - "route": "/spec.json", - "rule_filter": lambda rule: True, # all in - "model_filter": lambda tag: True, # all in - } - ], - "static_url_path": "/flasgger_static", - # "static_folder": "static", # must be set by user - "swagger_ui": True, - "specs_route": "/", -} + "host": "httpbin.org", # overrides localhost:5000 + "basePath": "/", # base bash for blueprint registration + "schemes": ["https"], + "protocol": "https", + "tags": [ + { + "name": "HTTP Methods", + "description": "Testing different HTTP verbs", + # 'externalDocs': {'description': 'Learn more', 'url': 'https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html'} + }, + {"name": "Auth", "description": "Auth methods"}, + { + "name": "Status codes", + "description": "Generates responses with given status code", + }, + {"name": "Request inspection", "description": "Inspect the request data"}, + { + "name": "Response inspection", + "description": "Inspect the response data like caching and headers", + }, + { + "name": "Response formats", + "description": "Returns responses in different data formats", + }, + {"name": "Dynamic data", "description": "Generates random and dynamic data"}, + {"name": "Cookies", "description": "Creates, reads and deletes Cookies"}, + {"name": "Images", "description": "Returns different image formats"}, + {"name": "Redirects", "description": "Returns different redirect responses"}, + { + "name": "Anything", + "description": "Returns anything that is passed to request", + }, + ], + } -swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config) + swagger_config = { + "headers": [], + "specs": [ + { + "endpoint": "spec", + "route": "/spec.json", + "rule_filter": lambda rule: True, # all in + "model_filter": lambda tag: True, # all in + } + ], + "static_url_path": "/flasgger_static", + # "static_folder": "static", # must be set by user + "swagger_ui": True, + "specs_route": "/", + } + + swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config) +else: + app.logger.warning("Swagger not found, legacy index will be used.") # Set up Bugsnag exception tracking, if desired. To use Bugsnag, install the # Bugsnag Python client with the command "pip install bugsnag", and set the @@ -244,6 +250,13 @@ def set_cors_headers(response): # ------ +if Swagger is None: + @app.route("/") + def no_flasgger_index(): + """Redirect to legacy index if flasgger is not available.""" + return redirect(url_for("view_landing_page")) + + @app.route("/legacy") def view_landing_page(): """Generates Landing Page in legacy layout.""" diff --git a/pyproject.toml b/pyproject.toml index c5bdb811..34e2c43d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ classifiers = [ dependencies = [ "brotlicffi", "decorator", - "flasgger", "flask >= 2.2.4", 'greenlet < 3.0; python_version<"3.12"', 'greenlet >= 3.0.0a1; python_version>="3.12.0rc0"', @@ -44,6 +43,9 @@ dependencies = [ [project.optional-dependencies] test = ["pytest", "tox"] +flasgger = [ + "flasgger", +] mainapp = [ "gunicorn", "gevent", diff --git a/tests/test_httpbin.py b/tests/test_httpbin.py index 6b751245..2ce63a61 100755 --- a/tests/test_httpbin.py +++ b/tests/test_httpbin.py @@ -10,6 +10,7 @@ from io import BytesIO import httpbin +from httpbin.core import Swagger from httpbin.helpers import parse_multi_value_header @@ -115,7 +116,8 @@ def setUp(self): def test_index(self): response = self.app.get('/', headers={'User-Agent': 'test'}) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, + 200 if Swagger is not None else 302) def get_data(self, response): if 'get_data' in dir(response):