Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 84 additions & 71 deletions httpbin/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = False

from . import filters
from .helpers import (
Expand Down Expand Up @@ -95,77 +98,83 @@ def jsonify(*args, **kwargs):

app.config["SWAGGER"] = {"title": "httpbin.org", "uiversion": 3}

template = {
"swagger": "2.0",
"info": {
"title": "httpbin.org",
"description": (
"A simple HTTP Request & Response Service."
"<br/> A <a href='http://kennethreitz.com/'>Kenneth Reitz</a> project."
"<br/> <br/> <b>Run locally: </b> <br/> "
"<code>$ docker pull ghcr.io/psf/httpbin</code> <br/>"
"<code>$ docker run -p 80:8080 ghcr.io/psf/httpbin</code>"
),
"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:
template = {
"swagger": "2.0",
"info": {
"title": "httpbin.org",
"description": (
"A simple HTTP Request & Response Service."
"<br/> A <a href='http://kennethreitz.com/'>Kenneth Reitz</a> project."
"<br/> <br/> <b>Run locally: </b> <br/> "
"<code>$ docker pull ghcr.io/psf/httpbin</code> <br/>"
"<code>$ docker run -p 80:8080 ghcr.io/psf/httpbin</code>"
),
"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_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)
swagger = Swagger(app, sanitizer=NO_SANITIZER, template=template, config=swagger_config)
else:
app.logger.warning(
"flasgger is not installed; serving the static landing page at / "
"and skipping the Swagger UI and /spec.json."
)

# Set up Bugsnag exception tracking, if desired. To use Bugsnag, install the
# Bugsnag Python client with the command "pip install bugsnag", and set the
Expand Down Expand Up @@ -243,8 +252,12 @@ def set_cors_headers(response):
# Routes
# ------

if Swagger:
staticroute = "/legacy"
else:
staticroute = "/"

@app.route("/legacy")
@app.route(staticroute)
def view_landing_page():
"""Generates Landing Page in legacy layout."""
return render_template("index.html")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,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"',
Expand All @@ -46,6 +45,7 @@ dependencies = [
[project.optional-dependencies]
test = ["pytest", "tox"]
mainapp = [
"flasgger",
"gunicorn",
"gevent",
]
Expand Down
27 changes: 27 additions & 0 deletions tests/test_httpbin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import base64
import subprocess
import unittest
import contextlib
import json
Expand Down Expand Up @@ -796,6 +798,31 @@ def test_etag_with_no_headers(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.headers.get('ETag'), 'abc')

def test_index_falls_back_to_static_page_without_flasgger(self):
"""When flasgger isn't installed, / serves the static legacy landing
page (HTTP 200) instead of the Swagger UI, rather than 500ing.

flasgger is imported at module load, so this runs in a subprocess that
poisons sys.modules['flasgger'] before httpbin is imported.
"""
code = (
"import sys\n"
"sys.modules['flasgger'] = None\n" # makes `import flasgger` raise ImportError
"import httpbin\n"
"assert httpbin.core.Swagger is False, repr(httpbin.core.Swagger)\n"
"r = httpbin.app.test_client().get('/')\n"
"assert r.status_code == 200, r.status_code\n"
"body = r.get_data(as_text=True)\n"
"assert 'httpbin(1): HTTP Client Testing Service' in body, 'legacy page not served'\n"
"assert 'swagger-ui' not in body, 'swagger UI unexpectedly rendered'\n"
"print('OK')\n"
)
result = subprocess.run(
[sys.executable, "-c", code], capture_output=True, text=True
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("OK", result.stdout)

def test_parse_multi_value_header(self):
self.assertEqual(parse_multi_value_header('xyzzy'), [ "xyzzy" ])
self.assertEqual(parse_multi_value_header('"xyzzy"'), [ "xyzzy" ])
Expand Down
Loading