Responseslink | top
The main goal of all WSGI middleware is to return a response corresponding to an HTTP or WSGI request. Responding in PoorWSGI is just like other known frameworks.
Returning valueslink | top
Just valuelink | top
The easiest way is to return a string or bytes. String values are automatically
converted to bytes, because it is WSGI internal. The HTTP Response is 200 OK with
text/html; charset=utf-8 content type and a default X-Powered-By header.
@app.route('/some/path')
def some_path(req):
return 'This is content for some path'
These examples return the same values.
@app.route('/other/path')
def some_path(req):
return b'This is content for some path'
Generatorlink | top
The second way is to return a generator. You can return any iterable object, but it must always be the first parameter; specifically, it cannot be a tuple! See Returned parameters. A generator must always return bytes!
@app.route('/list/of/bytes')
def list_of_bytes(req):
return [b'Hello ',
b'world!']
Or you can return any function that is a generator.
@app.route('/generator/of/bytes')
def generator_of_bytes(req):
def generator():
for i in range(10):
yield b'%d -> %x\n' % (i, i)
return generator()
Or the handler could be a generator.
@app.route('/generator/of/bytes')
def generator_of_bytes(req):
for i in range(10):
yield b'%d -> %x\n' % (i, i)
Returned parameterslink | top
In fact, you can return more than one value. You can return the content type, headers, and status code as additional parameters. Python returns all parameters as one tuple. There is no need to wrap them in brackets.
@app.route('/text/message')
def text_message(req):
return "Hello world!", "text/plain"
The first argument can still be a generator.
@app.route('/generator/of/bytes')
def generator_of_bytes(req):
def generator():
for i in range(10):
yield b'%d -> %x\n' % (i, i)
return generator(), "text/plain", () # empty headers
All values could look like this:
@app.route('/hello')
def hello(req):
return "Hello world!", "text/plain", ('X-Attribute', 'hello world'),
HTTP_OK
Returning Responseslink | top
make responselink | top
Response is the base class for returning values. In fact, other values which are returned from request handlers are converted to a Response object via the make_response function.
def make_response(data, content_type="text/html; character=utf-8", headers=None, status_code=HTTP_OK)
- data: str, bytes, dict, list, None or generator
Returned value as response body. Each type of data returns a different response type:
str, bytes - Response
dict, list - JSONResponse
None - NoContentReponse
generator - GeneratorResponse
- content_type: str
The
Content-Typeheader is set if this header is not already set in the headers.- headers: Headers, tuple, dict, ...
If it is a Headers instance, it will be set (e.g., referer). Other types are sent to the Headers constructor.
- status_code: int
HTTP status code, HTTP_OK is 200.
You can use the headers parameter instead of the content_type argument.
@app.http_state(NOT_FOUND) def not_found(req, *_): return make_response(b'Page not Found', headers={"Content-Type": "text/plain"}, status_code=NOT_FOUND)
If you return just a simple type, or a tuple of arguments, PoorWSGI automatically calls the make_response function to create a response for you.
@app.route("/json")
def not_found(req):
"""Return JSONResponse"""
return {"msg": "Message", "type": "object"}
@app.route('/gone')
def gone(req):
"""Return NoContentResponse"""
return None, "", None, HTTP_GONE
Responselink | top
A Response object is one of the basic elements of a WSGI application. Response is an object that contains all the necessary data to return a valid HTTP answer to the client: status code, text reason for the status code, headers, and body. That's all. All values returned from handlers are transformed to a Response object if possible. If a handler returns a valid Response, it will be returned as-is.
Response has some useful functionality, such as the write method for appending
to the body with auto-counting of Content-Length, and additional header
management.
@app.route('/teapot')
def teapot(req):
return Response("I'm teapot :-)", content_type="text/plain",
status_code=418)
There are some additional subclasses with specific functionality.
JSONResponselink | top
There is a JSONResponse class for quickly returning JSON.
@app.route('/json')
def teapot(req):
return JSONReponse(status_code=418, message="I'm teapot :-)",
numbers=list(range(5)))
This response returns the following data with status code 418:
{
"message": "I\'m teapot :-)",
"numbers": [0, 1, 2, 3, 4]
}
Or you can simply return a dictionary or a list. It will be automatically converted to JSONResponse by the make_response function. So it is similar to returning text or bytes.
Be careful that your dict or list has to be convertible to JSON by the json.dumps function.
@app.route('/dict')
def get_dict(_):
"""Return dictionary"""
return {"route": "/dict", "type": "dict"}
@app.route('/list')
def get_list(_):
"""Return list"""
return [["key", "value"], ["route", "/list"], ["type", "list"]]
JSONGeneratorResponselink | top
There is also a JSONGeneratorResponse class, which can return JSON and can accept generators as arrays. This response is streamed like GeneratorResponse, so data is not buffered in memory if the WSGI server does not buffer it.
@app.route('/json-generator')
def teapot(req):
return JSONGeneratorReponse(status_code=418, message="I'm teapot :-)",
numbers=range(5))
This response returns the following data with status code 418:
{
"message": "I\'m teapot :-)",
"numbers": [0, 1, 2, 3, 4]
}
FileResponselink | top
FileResponse opens the file and sends it through wsgi.filewrapper, which
could be a sendfile() call. See PEP 3333. Content type and length are read
from the system.
@app.route('/favicon.ico')
def favicon(req):
return FileResponse("/favicon.ico")
GeneratorResponselink | top
A Response that is used for generator values. A generator must return bytes, not strings. For a generator that returns strings, use StrGeneratorResponse, which encodes the strings to UTF-8 bytes.
NoContentResponselink | top
Sometimes you don't want a response payload. NoContentResponse has a default code of 204 No Content .
RedirectResponselink | top
A Response with an interface for a more comfortable redirect response.
@app.route("/old/url")
def old_url(req):
return RedirectResponse("/new/url", True)
NotModifiedResponselink | top
NotModifiedResponse is based on NoContentResponse with status code 304 Not Modified . You have to add a Not Modified header in the headers parameters or as a constructor argument.
from base64 import urlsafe_b64encode
from hashlib import md5
@app.route("/static/filename")
def static_url(req):
last_modified = int(getctime(req.document_root+"/filename"))
weak = urlsafe_b64encode(md5(last_modified.to_bytes(4, "big")).digest())
etag = f'W/"{weak.decode()}"'
if 'If-None-Match' in req.headers:
if etag == req.headers.get('If-None-Match'):
return NotModifiedResponse(etag=etag)
if 'If-Modified-Since' in req.headers:
if_modified = http_to_time(req.headers.get('If-Modified-Since'))
if last_modified <= if_modified:
return NotModifiedResponse(date=time_to_http())
return FileResponse(req.document_root+"/filename",
headers={'ETag': etag})
Partial Contentlink | top
Sometimes, you want to return partial content, which is a typical reaction to Range headers. For such situations, there are the parse_range function and the make_partial Response method.
@app.route("/last/100/bytes")
def last_bytes(req):
response = Response(os.urandom(1000))
response.make_partial({None, 100})
return response
@app.route("/var/log/messages")
def messages(req):
"""Return parts defined in request Range header."""
response = FileResponse("/var/log/messages")
if 'Range' in req.headers:
ranges = parse_range(req.headers['Range'])
if "bytes" in ranges:
response.make_partial(ranges["bytes"])
return response
PartialResponselink | top
For special use cases where a programmer has their own mechanism to select a range,
for example if units are not bytes, there is PartialResponse, which is similar
to Response, but is already set to 206 Partial Content, and you only need to
use the make_range method to create the correct Content-Range header.
@app.route("/some/range"):
def some_range(req):
"""Return 100 unicodes with right Content-Range header."""
response = PartialResponse(''.join(random.choices("ěščřžýáíé", k=100)))
response.make_range({100, 199}, "unicodes", 200)
return response
Stopping handlerslink | top
HTTPExceptionlink | top
There is the HTTPException class, based on Exception, which is used for stopping a handler with the correct HTTP status. There are two possible scenarios:
You want to stop with a specific HTTP status code, and a handler from the application will be used to generate the correct response.
@app.route("/some/url")
def some_url(req):
if req.is_xhr:
raise HTTPException(HTTP_BAD_REQUEST)
return "Some message", "text/plain"
Or you want to stop with a specific response. Instead of a status code, just use Response object.
@app.route("/other/url")
def some_url(req):
if req.is_xhr:
error = Response(b'{"reason": "Ajax not suported"}',
content_type="application/json",
status_code=HTTP_BAD_REQUEST)
raise HTTPException(error)
return "Other message", "text/plain"
Additional functionality
If the status code is DECLINED, it returns nothing. That means no status
code, no headers, no response body. Just stop the request.
If the status code is HTTP_NO_CONTENT, it returns NoContentResponse, so the
message body is not sent.
When the handler raises any other exception, it generates an Internal Server Error status code.
Compatibilitylink | top
For compatibility with old PoorWSGI and other WSGI middleware, there are two functions.
It has the same interface as RedirectResponse, and only raises the HTTPException with RedirectResponse.
It has the same interface as HTTPException, and voila, it raises the HTTPException.
Routinglink | top
There are two ways to set a path handler: via decorators of the Application object, or using a set_ method where one of the parameters is your handler. The choice depends on how your application is structured. If your web project has one or a few files where your handlers are, it is a good idea to use decorators. But if you have a large project with many files, it could be difficult to load all files with decorated handlers. In that case, set_ methods in a single file, such as a route file or dispatch table, is a better approach.
Static Routinglink | top
There is a method and a decorator to set your function (handler) to respond to a
static route: Application.set_route and Application.route. Both of them have
two parameters: first, the required path like /some/path/for/you, and second,
method flags, which default to METHOD_HEAD | METHOD_GET. There are other
methods in the state module like METHOD_POST, METHOD_PUT, etc. There are two
special constants: METHOD_GET_POST, which is HEAD | GET | POST, and METHOD_ALL,
which includes all supported methods. If the method does not match but the path
exists in the internal table, the HTTP state HTTP_METHOD_NOT_ALLOWED is
returned.
@app.route('/some/path')
def some_path(req):
return 'Data of some path'
def other_path(req):
return 'Data of other path'
app.set_route('/some/other/path', other_path, state.METHOD_GET_POST)
You can pop from the application table via method Application.pop_route, or get the internal table via Application.routes property. Each path can have only one handler, but one handler can be used for more paths.
Regular expression routeslink | top
As in other WSGI connectors (or frameworks, if you prefer), there is a way to define routes that capture part of the URL path as a parameter of the handler. PoorWSGI calls them regular expression routes. You can use them in a nice human-readable form or in your own regular expressions. Basic use is defined by group name.
# group regular expression
@app.route('/user/<name>')
def user_detail(req, name):
return 'Name is %s' % name
Filters are defined by regular expressions from the Application.filters table.
Each filter is used to transform a URL group into a regular expression. The default
filter is r'[^/]+' with a str convert function. You can use any filter
from the filters table.
# group regular expression with filter
@app.route('/<surname:word>/<age:int>')
def surnames_by_age(req, surname, age):
return 'Surname is: %s and age is: %d' % (surname, age)
The :int filter is defined by r'-?\d+' with the int conversion function.
So age must be a number and the input parameter is an int instance.
There are predefined filters, for example: :int, :word, :re:, and
none as the default filter. :word is defined as the r'\w+' regular
expression, and PoorWSGI uses the re.U flag, so it matches any Unicode string
(i.e., UTF-8 string). For all filters, see the Application.filters property or
the /debug-info page.
You can get a copy of the filters table by calling the Application.filters
property. This filters table is output to the debug-info page. Adding your own
filter is possible with the set_filter function, which takes a name, a
regular expression, and a convert function (which is str by default). You can
then use this filter in a group regular expression.
app.set_filter('email', r'[a-zA-Z\.\-]+@[a-zA-Z\.\-]+', str)
@app.route('/user/<login:email>')
def user_by_login(req, login):
return 'Users email is %s' % login
Alternatively, you can use filters defined by inline regular expressions. That is
the :re: filter. This filter takes a regular expression that you provide, and
always uses the str convert function, so the parameter is always a string.
@app.route('/<number:re:[a-fA-F\d]+>')
def hex_number(req, number):
return ('Number is %s that is %d so %x' %
(number, int(number,16), int(number,16)))
Group naminglink | top
Group names must be unique in the defined path. They are stored in an
ordered dictionary and wrapped by their convert functions. You can name them
in the route definition as you wish; they do not need to match the parameter
names in the handler, but they must maintain the same ordering. Be careful not to
name parameters in the handler with a Python keyword, like class for example. If
you prefer, you can use Python's "varargs" syntax to receive any number of parameters
in your handler function.
@app.route('/test/<variable0>/<variable1>/<variable2>')
def test_varargs(req, *args):
return "Parse %d parameters %s" % (len(args), str(args))
A future feature of regular expression routes is direct access to the dictionary
with the req.groups variable. This variable is set from any regular
expression route.
@app.route('/test/<variable0>/<variable1>/<variable2>')
def test_varargs(req, *args):
return "All input variables from url path: %s" % str(req.groups)
Regular expression routes, like static routes, can be set with Application.route or Application.set_route methods. Internally, however, Application.regular_route or Application.set_regular_route is called. The same situation applies to Application.pop_route and Application.pop_regular_route.
Other handlerslink | top
Default handlerlink | top
If no route matches, two scenarios can occur. The first is to call the default handler if the method matches. The default handler is set with the default Application decorator or Application.set_default method. The parameter is only the method, which also defaults to METHOD_HEAD | METHOD_GET. Unlike route handlers, when the method does not match, a 404 error is returned.
So the default handler is a fallback with the r'/.*' regular expression. For
example, you can use it for any OPTIONS method.
@app.default(METHOD_OPTIONS): def default(req): return b'', '', {'Allow': 'OPTIONS', 'GET', 'HEAD'}
Be careful: the default handler is called before the 404 not found handler. If it is possible to serve the request in any other way, it will be. For example, if poor_DocumentRoot is set and PoorWSGI finds the file, it will be sent. Of course, the internal file or directory handler is used only with METHOD_GET or METHOD_HEAD.
HTTP state handlerslink | top
There are some predefined HTTP state handlers, which are used when other HTTP states are raised via HTTPException or any other exception that ends with an HTTP_INTERNAL_SERVER_ERROR status code.
You can define your own handlers for any combination of status code and method type, similar to route handlers. Responses from these handlers are the same as in route handlers.
Note that some HTTP state handlers receive additional keyword arguments.
@app.http_state(state.HTTP_NOT_FOUND) def page_not_found(req, *_): return "Your request %s not found." % req.path, "text/plain"
If your HTTP state (error) handler raises an error, a 500 Internal Server Error is returned and the default internal server error handler is called. If your default internal server error handler crashes as well, the built-in PoorWSGI internal server error handler is called.
Error handlerslink | top
In most cases, when an exception is raised from your handler, Internal Server Error is returned from the server. When you want to handle each type of exception, you can define your own error handler, which will be called instead of the HTTP_INTERNAL_SERVER_ERROR state handler.
class MyValueError(ValueError)
pass
@app.error_handler(ValueError)
def value_error(req, error):
"""This is called when value error was raised."""
return "Value Error: %s" % error, state.HTTP_BAD_REQUEST
@app.route('/value/<value:int>')
def value_handler(req, value)
if value != 42:
raise MyValueError("Not a valid value")
return "Yep!"
Exception handlers are stored in an OrderedDict, so the exception type is checked in the same order as you set error handlers. Therefore, you must define the handler for the base exception last.
Before and After responselink | top
PoorWSGI also has two special lists of handlers. The first iterates and calls before each response. You can add functions with Application.before_response and Application.after_response decorators or Application.add_before_response and Application.add_after_response methods. There are also Application.pop_before_response and Application.pop_after_response methods to remove handlers.
Before response handlers are called in the order they were added to the list. Their return values are ignored. If they raise an error, an Internal Server Error is returned and the HTTP state handler is called.
After response handlers are called in the order they were added to the list. If they raise an error, an Internal Server Error is returned and the HTTP state handler is called, but all code from the before response list and from the route handler has already been executed.
An after response handler is called even if an error handler, such as internal_server_error, was called.
A before response handler must have a request argument, but an after response handler must have request and response arguments.
@app.before_response() def before_each_response(request): ... @app.after_response() def after_each_response(request, response): ...
Filteringlink | top
TODO: How to write an output filter, gzip for example...
WebSocketslink | top
WebSockets are not directly supported in PoorWSGI, but upgrade requests can be handled like other HTTP requests. See the websocket.py example, which uses the uWSGI implementation or WSocket implementation.
Request variableslink | top
PoorWSGI has two classes for parsing request arguments: one for arguments from the request path (typical for GET requests) and one for arguments from the request body (typical for POST requests). This parsing is enabled by default, but you can configure it with options.
Query argumentslink | top
Request query arguments are stored in the Args class, defined in the
poorwsgi.request module. Args is a dict-based class with the getfirst and
getlist methods. You can access query variables via req.args whenever
poor_AutoArgs is set to On, which is the default.
@app.route('/test/get')
def test_get(req)
name = req.args.getfirst('name')
colors = req.args.getlist('color', func=int)
return "Get arguments are %s" % str(req.args)
If no arguments are parsed, or if poor_AutoArgs is set to Off, req.args is an EmptyForm instance, which is also a dict-based class with both methods.
Form argumentslink | top
Request form arguments are stored in the FieldStorage class, defined in the
poorwsgi.fieldstorage module. This class is inspired by FieldStorage from the
legacy cgi module. Variables are parsed whenever poor_AutoForm is set to
On (which is the default), the request method is POST, PUT or PATCH, and the
request MIME type is one of Application.form_mime_types . You can also trigger
this parsing for other methods, but wsgi.input must exist in the request
environment from the WSGI server.
The req.form instance is created with poor_KeepBlankValues and
poor_StrictParsing variables, just as the Args class is created. However,
FieldStorageParser has a file_callback variable, which is configurable by the
Application.file_callback property.
@app.route('/test/post', methods = state.METHOD_GET_POST)
def test_post(req)
id = req.args.getfirst('id', 0, int) # id is get from request uri and it
# is convert to number with zero
# as default
name = req.form.getfirst('name')
colors = req.form.getlist('color', func=int)
return "Post arguments for id are %s" % (id, str(req.args))
Similar to the Args class, if poor_AutoForm is set to Off, or if the method is
not POST, PUT or PATCH, req.form is an EmptyForm instance instead of
FieldStorage.
JSON requestlink | top
Initially, JSON requests came from AJAX. There is automatic JSON parsing in the Request object, which parses the request body to a JSON variable. This parsing starts only when the Application.auto_json variable is set to True (default) and the MIME type of a POST, PUT or PATCH request is application/json. Then the request body is parsed to the json property. You can configure JSON types via the Application.json_mime_types property, which is a list of request MIME types.
import json
@app.route('/test/json',
methods=state.METHOD_POST | state.METHOD_PUT | state.METHOD_PATCH)
def test_json(req):
for key, val in req.json.items():
req.error_log('%s: %s' % (key, str(val)))
res = Response(content_type='application/json')
json.dump(res, {'Status': '200', 'Message': 'Ok'})
return res
JQuery AJAX request could look like this:
$.ajax({ url: '/test/json',
type: 'put',
accepts : {json: 'application/json', html: 'text/html'},
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({'test': 'Test message',
'count': 42, 'note': null}),
success: function(data){
console.log(data);
},
error: function(xhr, status, http_status){
console.error(status);
console.error(http_status);
}
});
There are a few variants that req.json could be:
JsonDict when a dictionary is parsed.
JsonList when a list is parsed.
Other base types from the json.loads function, such as str, int, float, bool, or None.
None when JSON parsing fails. This is logged with a WARNING log level.
File uploadinglink | top
By default, FieldStorage stores files somewhere in the /tmp directory. This
happens in FieldStorageParser, which calls TemporaryFile. Uploaded files
are accessible like other form variables, but:
Any variable from FieldStorage is accessible with the __getitem__ method. So
you can get a variable by req.form[key], which returns a FieldStorage
instance. This instance has some attributes that you can use to test what type
of variable it is.
@app.route('/test/upload', methods = state.METHOD_GET_POST)
def test_upload(req):
# store file from upload variable to my_file_storage file
if 'upload' in req.form and req.form['upload'].filename:
with open('my_file_storage', 'w+b') as f:
f.write(req.form['upload'].file.read())
Own file callbacklink | top
Sometimes, you want to use your own file_callback because you don't want to use
TemporaryFile as storage for uploaded files. You can do it by simply adding a
class that is an io.FileIO class in Python 3.x. Then, only set the
Application.file_callback property.
from poorwsgi import Application from io import FileIO app = Application('test') app.file_callback = FileIO
As you can see, this example works, but it is a poor solution to your problem.
A better solution is to store files only if they do not already exist, in a
configurable directory. You need to use a factory to create file_callback. The
following example shows custom form processing; however, this is not necessary
since file_callback can be set directly via an Application property.
from io import FileIO from os.path import exists from poorwsgi import Application, state, fieldstorage app = Application('test') class Storage(FileIO): def __init__(self, directory, filename): self.path = directory + '/' + filename if exists(self.path): raise Exception("File %s exist yet" % filename) super(Storage, self).__init__(self.path, 'w+b') class StorageFactory: def __init__(self, directory): self.directory = directory if not exists(directory): os.mkdir(directory) def create(self, filename): return Storage(self.directory, filename) # disable automatic request body parsing - IMPORTANT ! app.auto_form = False @app.before_response() def auto_form(req): """ Own implementation of req.form paring before any POST response with own file_callback. """ if req.method_number == state.METHOD_POST: factory = StorageFactory('./upload') try: parser = FieldStorageParser( req.input, req.heades, keep_blank_values=app.keep_blank_values, strict_parsing=app.strict_parsing, file_callback=factory.create) req.form = parser.parser() except Exception as e: req.log_error(e)
CachedInputlink | top
When HTTP forms are base64 encoded, FieldStorageParser uses readline on the
request input file. This is not optimal. CachedInput is a class that serves as
a wrapper around the wsgi.input file to address this.
Process variableslink | top
Here are the application variables used to configure request processing.
Application.auto_argslink | top
If auto_args is set to True (which is the default), the Request object
parses input arguments from the request URI at initialization. There will be a
Request.args property, which is an instance of the Args class. If you want
to disable this functionality, set this property to False. If argument
parsing is disabled, Request.args will be an instance of EmptyForm with
the same interface and no data.
Application.auto_formlink | top
If auto_form is set to True (which is the default), the Request object
parses input arguments from the request body at initialization when the request
type is POST, PUT or PATCH. There will be a Request.form property which is
an instance of the FieldStorage class. If you want to disable this
functionality, set this property to False. If form parsing is disabled, or
JSON is detected, Request.form will be an instance of EmptyForm with the
same interface and no data.
Application.form_mime_typeslink | top
List of MIME types, which is parsed as an input form by the
FieldStorageParser class. If the input request does not have one of these
MIME types set, that form will not be parsed.
Application.file_callbacklink | top
A class or function that is used to store a file from the form. See own file callback for more details.
Application.auto_jsonlink | top
If it is True (which is the default), the method is POST, PUT or PATCH and
the request mime type is JSON, then the Request object automatically parses
the request body to the Request.json dict property. If it is disabled, or if
a form is detected, then an EmptyForm instance is set.
Application.json_mime_typeslink | top
List of MIME types, which is parsed as JSON by the json.loads function.
If the input request does not have one of these MIME types set, then
Request.json will not be parsed.
Application.keep_blank_valueslink | top
This property is passed to the Args and FieldStorageParser classes when
auto_args and auto_form are set, respectively.
By default, this property is set to 0. If it is set to 1, blank values
will be interpreted as empty strings.
Application.strict_parsinglink | top
This property is passed to the Args and FieldStorageParser classes when
auto_args and auto_form are set, respectively.
By default, this variable is set to 0. If it is set to 1, a ValueError
exception may be raised on a parsing error. You will almost certainly never want to
set this variable to 1; if you do, use it in your own parsing.
app.auto_form = False
app.auto_args = False
app.strict_parsing = 1
@app.before_response()
def auto_form_and_args(req):
""" This is own implementation of req.form and req.args paring """
try:
req.args = request.Args(req,
keep_blank_values=app.keep_blank_values,
strict_parsing=app.strict_parsing)
except Exception as e:
loging.error("Bad request uri: %s", e)
if req.method_number == state.METHOD_POST:
try:
parser = fieldstorage.FieldStorageParser(
req.input, req.headers,
keep_blank_values=app.keep_blank_values,
strict_parsing=app.strict_parsing)
req.form = parser.parse()
except Exception as e:
logging.error("Bad request body: %s", e)
Application.auto_cookieslink | top
When auto_cookies is set to True (which is the default), the
Request.cookies property is set when the request headers contain a Cookie
header. Otherwise, an empty tuple will be set.
Application / User optionslink | top
Like mod_python's Request, the PoorWSGI Application has a get_options method.
This method returns a dictionary of application options, whose names start with
the app_ prefix. This prefix is stripped from the option names.
[uwsgi] # uwsgi config example ... env = app_db_file = mywebapp.db # variable is db_file env = app_tmp_path = tmp # variable is tmp_path env = app_templ = templ # variable is templ
And you can get these variables with get_options method:
config = app.get_options()
@app.route('/options')
def list_options(req):
return ("%s = %s" % (key, val) in config.items())
The output of application URL /options looks like this:
db_file = mywebapp.db tmp_path = tmp templ = templ
You can also store your variables in the request object. There are a few reserved
variables for you, which PoorWSGI never uses, and which are None by default:
| req.user: | For user object, who is login, check_digest decorator set this variable. |
|---|---|
| req.api: | For API checking. OpenAPIRequest use this variable. |
| req.db: | For a single database connection per request. You can store a structure with multiple databases if needed. |
| req.app_: | As a prefix for any of your application variables. |
So if you want to add any other variable, be careful how you name it.
Headers and Sessionslink | top
Request Headerslink | top
Request headers were introduced earlier; this section provides more detail.
The Request object has a headers_in attribute, which
is an instance of wsgiref.headers.Headers. These headers contain the request
headers from the client, similar to mod_python. You can read them as needed.
In addition, there are some Request properties for accessing parsed header values.
| headers: | Full headers object. |
|---|---|
| mime_type: | Return mime type part from |
| charset: | Return charset part from |
| content_length: | Return content length if |
| accept: | List of |
| accept_charset: | List of |
| accept_encoding: | List of |
| accept_language: | List of |
| accept_html: | True if |
| accept_xhtml: | True if |
| accept_json: | True if |
| is_xhr: | True if |
| cookies: | Cookie object created from |
| authorization: | Parsed |
| referer: | HTTP referer from |
| user_agent: | User's client from |
| forwarded_for: | Value of |
| forwarded_host: | Value of |
| forwarded_proto: | Value of |
Response Headerslink | top
Response headers use the same Headers class as in the request object.
If you don't set a header when you create a Response object,
the default X-Powered-By header is set to "Poor WSGI for Python". The
Content-Type and Content-Length headers are appended automatically. Each
header key must appear at most once, except for Set-Cookie, which can be set
multiple times.
@app.route('/some/path')
def some_path(req):
xparam = int(req.headers.get('X-Param', '0'))
# res.headers will have X-Powered-By, Content-Type and Content-Length
res = Response("O yea!", content_type="text/plain")
# res.headers["S-Param"] = "00" by default
res.add_header("S-Param", xparam*2)
return res
Sessionslink | top
Like mod_python, PoorSession is the session class of PoorWSGI. It's a
self-contained cookie with a data dictionary. Data are sent to the client
in a hidden, bzip2-compressed, base64-encoded format. PoorSession needs a secret_key,
which can be set by the poor_SecretKey environment variable to the
Application.secret_key property.
from functools import wraps from os import urandom import logging as log from poorwsgi import Application, state, redirect from poorwsgi.session import PoorSession app = Application('test') app.secret_key = urandom(32) # random secret_key def check_login(fn): @wraps(fn) # using wraps make right/better /debug-info page def handler(req): cookie = PoorSession(app.secret_key) cookie.load() if "passwd" not in cookie.data: # expires or didn't set log.info("Login cookie not found.") redirect("/login", message=b"Login required") return fn(req) return handler @app.route('/login', method=state.METHOD_GET_POST) def login(req): if req.method == 'POST': passwd = req.form.getfirst('passwd', func=str) if passwd != 'SecretPasswds': log.info('Bad password') redirect('/login', text='Bad password') response = RedirectResponse("/private/path") cookie = PoorSession(app.secret_key) cookie.data['passwd'] = passwd cookie.header(response) abort(response) return 'some html login form' @app.route('/private/path') @check_login def private_path(req): return 'Some private data' @app.route('/logout') def logout(req): response = RedirectResponse("/login") cookie = PoorSession(app.secret_key) cookie.destroy() cookie.header(response) return response
HTTP Digest Authlink | top
PoorWSGI supports HTTP Digest Authorization from version 2.3.x. Supported features are:
MD5, MD5-sess, SHA-256, SHA-256-sess algorithm, MD5-sess is default
none or auth quality of protection (qop), auth is default
nonce value timeout, so new hash will be count every N seconds, 300 sec (5min) is default
The
ncheader value from the browser is not currently checked on the server side.
Application settingslink | top
There are some application options that are used for HTTP Authorization configuration.
secret_key: Secret Key is used for generating
noncevalue, which is server side token.auth_type: At this moment, only
Digestvalue can be set.auth_algorithm: You can choose algorithm type for hash computing. But most browser understand only
MD5orMD5-sess, which is default.SHA256is supported by PoorWSGI too.auth_qop: Only
authis supported. You can switch off it, when you set it toNoneor empty string.auth_timeout: You can set timeout for
noncetoken, so browser must generate new hash values at least each timeout value.auth_map: Must be dictionary of dictionary of users digests. You can use PasswordMap, which has some additional methods for managing it, and save to / load from standard digest files.
from poorwsgi import Application app = Application(__name__) # secret key must set before auth_type app.secret_key = sha256(str(time()).encode()).hexdigest() app.auth_type = 'Digest' app.auth_map = PasswordMap('test.digest') app.auth_map.load() # load table from test.digest file
Usagelink | top
There is a check_digest decorator, which can be used to check the
Authorization header in client requests. Be careful when overriding the
default HTTP_UNAUTHORIZED handler - it must return the correct
WWW-Authenticate header when the browser does not send a valid
Authorization header.
@app.route('/admin_zone')
@check_digest('Admin Zone')
def admin_zone(req):
"""Page only for *Admin Zone* realm."""
return "You are %s user" % req.user.
@app.route('/user')
@check_digest('User Zone', 'foo')
def user_only(req):
"""Page only for *foo* user in *User Zone* only."""
...
The poorwsgi.digest module can also be used for managing the digest file. You can also manage PasswordMap directly with its methods.
python3 -m poorwsgi.digest -c digest.passwd 'User Zone' bfu ... # see full help python3 -m poorwsgi.digest -h
Debugginglink | top
Poor WSGI has a few debugging mechanisms you can use. First, it is a good idea to set the poor_Debug variable. If this variable is set, there is a full traceback on the internal_server_error page (HTTP 500).
The second effect of this variable is enabling the special debug page at
/debug-info URL. On this page, you can find:
a full handlers table with request paths, HTTP methods, and handlers that are called to serve those requests.
an HTTP state handlers table with HTTP status codes, HTTP methods, and handlers that are called when those HTTP states are raised.
the request headers sent by your browser when you access the debug page.
the Poor WSGI configuration variables for the current application instance.
application variables, which are set like connector variables but with the app_ prefix.
the request environment, which is passed from the WSGI server to the WSGI application, that is, to the Poor WSGI connector.
Profilinglink | top
If you want to profile your request code, you can do so with a profiler. The Poor WSGI application object has methods to set up profiling. You only need to provide a runctx function, which is called before every request. For each request, a .profile dump file will be generated for analysis.
If you want to profile the entire process from the start of your application, you can create a file that profiles the import of your application, which in turn imports the Poor WSGI connector.
import cProfile
# this import your application, which import Poor WSGI, so you can profile
# first server init, which is do, when server import your application.
# don't forget to import this file instead of simple.py or your
# application file
cProfile.runctx('from simple import *', globals(), locals(),
filename="log/init.profile")
# and this sets profiling of any request which is server by your
# web application
app.set_profile(cProfile.runctx, 'log/req')
When you use this file instead of your application file (simple.py for example), the application creates files in the log directory. The first file will be init.profile, created from the initial import by the WSGI server. Other files will look like req_.profile, req_debug-info.profile, etc. The second parameter of the set_profile method is the prefix for output file names. File names are created from the URL path, so each URL creates its own file.
There is a useful tool to view these profile files called runsnakerun. You can download it from http://www.vrplumber.com/programming/runsnakerun/. Using it is very simple - just open a profile file:
$~ python runsnake.py log/init.profile $~ python runsnake.py log/req_.profile
OpenAPIlink | top
OpenAPI aka Swagger 3.0 is a specification for RESTful API documentation and
request and response validation. PoorWSGI has an
openapi_core wrapper in the
openapi_wrapper module. You only need to declare your before and after
response handlers.
This wrapper is the only place where the openapi_core Python package is used, so it is not in PoorWSGI's requirements. You need to install it separately:
$~ pip install openapi_core
Example code of usage:
from os import path import json import logging from openapi_core import create_spec from openapi_core.validation.request.validators import RequestValidator from openapi_core.validation.response.validators import ResponseValidator from openapi_core.schema.operations.exceptions import InvalidOperation from openapi_core.schema.servers.exceptions import InvalidServer from openapi_core.schema.paths.exceptions import InvalidPath from poorwsgi import Application from poorwsgi.response import Response, abort from poorwsgi.openapi_wrapper import OpenAPIRequest, OpenAPIResponse app = Application("OpenAPI3 Test App") request_validator = None response_validator = None with open(path.join(path.dirname(__file__), "openapi.json"), "r") as openapi: spec = create_spec(json.load(openapi)) request_validator = RequestValidator(spec) response_validator = ResponseValidator(spec) @app.before_response() def before_each_response(req): req.api = OpenAPIRequest(req) result = request_validator.validate(req.api) if result.errors: errors = [] for error in result.errors: if isinstance(error, (InvalidOperation, InvalidServer, InvalidPath)): logging.debug(error) return # not found errors.append(repr(error)+":"+str(error)) abort(Response(json.dumps({"error": ';'.join(errors)}), status_code=400, content_type="application/json")) @app.after_response() def after_each_response(req, res): """Check answer by OpenAPI specification.""" result = response_validator.validate( req.api or OpenAPIRequest(req), OpenAPIResponse(res)) for error in result.errors: if isinstance(error, InvalidOperation): continue logging.error("API output error: %s", str(error)) return res
Of course, you need openapi.json file with OpenAPI specification, where you
specified your API.