I have the following code which converts a HTML file upload to a PDF using a Python Flask web server REST endpoint. The file is uploaded as multi-part form data. It works great, but when I tried to add Swagger to the REST endpoint using flask_restx
it broke. It seems to interpret the output PDF as JSON (even though I tried to tell flask_restx
that it must produce a PDF in several ways) because it gives me the error at the end. I even wrote out the PDF to disk to check that it still works and it does. Where am I going wrong?
import io
import os
from flask import (Flask, redirect, render_template, render_template_string, request,
send_from_directory, url_for, make_response, jsonify)
from xhtml2pdf import pisa
from io import StringIO
#from flask_httpauth import HTTPBasicAuth
from flask_restx import Api, Resource, fields
from werkzeug.datastructures import FileStorage
# Create a Flask website app.
app = Flask(__name__)
# Add Basic Authentication to the Flask app.
#auth = HTTPBasicAuth()
# Add Swagger support to the Flask app.
api = Api(
app = app,
version = '1.0', # REST API version not Swagger version (Swagger = 2.0)
description = 'HTML to PDF API',
doc = '/swagger/',
default = 'api',
default_label = 'HTML to PDF API'
)
# Create a file upload parser for the HTML file via Swagger.
upload_parser = api.parser()
upload_parser.add_argument('file', location='files', type=FileStorage, required=True)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/hello', methods=['POST'])
def hello():
name = request.form.get('name')
if name:
return render_template('hello.html', name = name)
else:
return redirect(url_for('index'))
#@auth.verify_password
def authenticate(username, password):
"""Authenticate the user via Basic Authentication."""
if username and password:
if username == 'username' and password == 'password':
return True
else:
return False
else:
return False
return False
class Pdf():
def render_pdf(self, html):
"""Render HTML to PDF using xhtml2pdf"""
pdf = io.BytesIO()
pisa.CreatePDF(StringIO(html), pdf)
return pdf.getvalue()
# Data model for the successful PDF response
pdf_model = api.model('PDFResponse', {
'pdf_data': fields.Raw # Assuming the 'pdf_data' will be the raw bytes of the PDF file.
})
@api.route("/ConvertHTMLtoPDF", methods=["POST"])
#@auth.login_required
class HTMLtoPDF(Resource):
@api.expect(upload_parser)
@api.response(200, 'Success', pdf_model)# headers={'content-type': 'application/pdf', 'content-disposition': 'attachment; filename=receiving.pdf'})
@api.produces(['application/pdf'])
# @api.response(422, 'Unprocessable Content', headers={'content-type': 'application/json'})
def post(self):
"""Upload a HTML file via a REST POST and return a PDF of that HTML.
---
consumes: ["multipart/form-data"]
produces: ["application/pdf"]
parameters:
- in: formData
name: file
type: file
required: true
description: upload a HTML file
"""
if request.method == "POST":
file_content = None
# file = request.files.get("file")
#for f in request.files.getlist('file'):
# # file_content = file.read()
# # Read the HTML posted. In Postman use body, form-data with key=file and value=html file after choosing type file on key.
# file_content = f.read()
# # Only read one file
# break
args = upload_parser.parse_args()
file_content = args['file'] # This is FileStorage instance
# Check if file loaded successfully or not.
if file_content:
# Renders a template from the given template source string with the given context. Template variables will be autoescaped.
# Note: HTML must be in ANSI latin-1.
# html = render_template_string(file_content.decode("latin-1"))
s = file_content.read().decode("latin-1")
print(s)
html = render_template_string(s)
file_class = Pdf()
pdf = file_class.render_pdf(html)
fout = open('c:/downloads/test.pdf', 'wb')
fout.write(pdf)
fout.close()
headers = {
'content-type': 'application/pdf',
'content-disposition': 'attachment; filename=receiving.pdf'}
return pdf, 200, headers
else:
return jsonify(message="Upload unsuccessful"), 422
# Render the index.html if file was not posted.
return render_template("index.html")
# Run the Flask web app.
if __name__ == '__main__':
app.run()
The error from the flask server log:
expected string or bytes-like object
[2023-08-04 15:38:08,087] ERROR in app: Exception on /ConvertHTMLtoPDF [POST]
Traceback (most recent call last):
File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask\app.py", line 1516, in full_dispatch_request
rv = self.dispatch_request()
File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask\app.py", line 1502, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask_restx\api.py", line 408, in wrapper
return self.make_response(data, code, headers=headers)
File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask_restx\api.py", line 432, in make_response
resp = self.representations[mediatype](data, *args, **kwargs)
File "D:\EBackup\Work\Python\azure\GMTPythonFunctionApps\.venv\lib\site-packages\flask_restx\representations.py", line 22, in output_json
dumped = dumps(data, **settings) + "\n"
File "C:\Python310\lib\json\__init__.py", line 231, in dumps
return _default_encoder.encode(obj)
File "C:\Python310\lib\json\encoder.py", line 199, in encode
chunks = self.iterencode(o, _one_shot=True)
File "C:\Python310\lib\json\encoder.py", line 257, in iterencode
return _iterencode(o, 0)
File "C:\Python310\lib\json\encoder.py", line 179, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type bytes is not JSON serializable