diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 0000000..8ec01bb --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,32 @@ +======== +Examples +======== + +The foldlers here contain examples of how to run the +StackInAWSGI functionality in some common WSGI servers. + +gunicorn +-------- + +Shows how to run StackInAWSGI using Gunicorn. + +uwsgi +----- + +Shows how to run StackInAWSGI using uWSGI. + +python-wsgiref +-------------- + +Shows how to run StackInAWSGI using the built-in wsgiref. + + +Known Issues +============ + +- Only 1 worker can be used for any given WSGI server platform. + This is due to the fact that the session information is not + shared between workers. In theory all the workers should be able + to see the data; however, they may be running as separate processes + which would limit their value of what they can see. More research + is needed to overcome this limitation. diff --git a/examples/gunicorn/app.py b/examples/gunicorn/app.py index e2d6063..d675173 100644 --- a/examples/gunicorn/app.py +++ b/examples/gunicorn/app.py @@ -1,10 +1,17 @@ """ Gunicorn Example App """ +import logging from stackinabox.services.hello import HelloService from stackinawsgi import App -app = App([HelloService()]) -app.StackInABoxUriUpdate('localhost:8081') +lf = logging.FileHandler('stackinawsgi.log') +lf.setLevel(logging.DEBUG) +log = logging.getLogger() +log.addHandler(lf) +log.setLevel(logging.DEBUG) + +app = App([HelloService]) +app.StackInABoxUriUpdate('http://localhost:8081') diff --git a/examples/gunicorn/gunicorn_start.sh b/examples/gunicorn/gunicorn_start.sh index 25c1c72..47ee804 100755 --- a/examples/gunicorn/gunicorn_start.sh +++ b/examples/gunicorn/gunicorn_start.sh @@ -1,14 +1,46 @@ #!/bin/bash +# Note: Until Issue #11 (Session Bug) is fully resolved, +# Stack-In-A-WSGI only supports a single worker, +# each worker will have its own session data completely +# independent of all other workers. +WORKER_COUNT=1 VENV_DIR="gunicorn_example_app" +for ARG in ${@} +do + echo "Found argument: ${ARG}" + if [ "${ARG}" == "--reset" ]; then + echo " User requested virtualenv reset, long argument name" + let -i RESET_VENV=1 + elif ["${ARG}" == "-r" ]; then + echo " User requested virtualenv reset, short argument name" + let -i RESET_VENV=2 + fi +done + +MD5SUM_ROOT=`ls / | md5sum | cut -f 1 -d ' '` +MD5SUM_VENV=`ls ${VENV_DIR} | md5sum | cut -f 1 -d ' '` +if [ "${MD5SUM_ROOT}" == "${MD5SUM_VENV}" ]; then + echo "Virtual Environment target is root. Configuration not supported." + exit 1 +fi + +if [ -v RESET_VENV ]; then + echo "Checking for existing virtualenv to remove..." + if [ -d ${VENV_DIR} ]; then + echo "Removing virtualenv ${VENV_DIR}..." + rm -Rf ${VENV_DIR} + fi +fi + if [ ! -d ${VENV_DIR} ]; then + echo "Building virtualenv..." virtualenv ${VENV_DIR} INITIALIZE_VENV=1 fi - source ${VENV_DIR}/bin/activate if [ -v INITIALIZE_VENV ]; then @@ -16,4 +48,4 @@ if [ -v INITIALIZE_VENV ]; then fi echo "Starting new instances..." -gunicorn -b 127.0.0.1:8081 -w 16 --error-logfile app-errors.log --access-logfile app-access.log --log-level DEBUG -D app:app +gunicorn -b 127.0.0.1:8081 -w ${WORKER_COUNT} --error-logfile app-errors.log --access-logfile app-access.log --log-level DEBUG -D app:app diff --git a/examples/python-wsgi/__init__.py b/examples/python-wsgi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/python-wsgi/app.py b/examples/python-wsgi/app.py new file mode 100644 index 0000000..9c84231 --- /dev/null +++ b/examples/python-wsgi/app.py @@ -0,0 +1,17 @@ +""" +Python WSGI-Ref Example App +""" +import logging + +from stackinabox.services.hello import HelloService + +from stackinawsgi import App + +lf = logging.FileHandler('python-wsgi-ref.log') +lf.setLevel(logging.DEBUG) +log = logging.getLogger() +log.addHandler(lf) +log.setLevel(logging.DEBUG) + +app = App([HelloService]) +app.StackInABoxUriUpdate('localhost:8081') diff --git a/examples/python-wsgi/pywsgiref_restart.sh b/examples/python-wsgi/pywsgiref_restart.sh new file mode 100755 index 0000000..c4de6d7 --- /dev/null +++ b/examples/python-wsgi/pywsgiref_restart.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +pywsgiref_stop.sh +pywsgiref_start.sh diff --git a/examples/python-wsgi/pywsgiref_start.sh b/examples/python-wsgi/pywsgiref_start.sh new file mode 100755 index 0000000..05d11b0 --- /dev/null +++ b/examples/python-wsgi/pywsgiref_start.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Note: Until Issue #11 (Session Bug) is fully resolved, +# Stack-In-A-WSGI only supports a single worker, +# each worker will have its own session data completely +# independent of all other workers. +WORKER_COUNT=1 +VENV_DIR="pywsgiref_example_app" + +for ARG in ${@} +do + echo "Found argument: ${ARG}" + if [ "${ARG}" == "--reset" ]; then + echo " User requested virtualenv reset, long argument name" + let -i RESET_VENV=1 + elif ["${ARG}" == "-r" ]; then + echo " User requested virtualenv reset, short argument name" + let -i RESET_VENV=2 + fi +done + +MD5SUM_ROOT=`ls / | md5sum | cut -f 1 -d ' '` +MD5SUM_VENV=`ls ${VENV_DIR} | md5sum | cut -f 1 -d ' '` +if [ "${MD5SUM_ROOT}" == "${MD5SUM_VENV}" ]; then + echo "Virtual Environment target is root. Configuration not supported." + exit 1 +fi + +if [ -v RESET_VENV ]; then + echo "Checking for existing virtualenv to remove..." + if [ -d ${VENV_DIR} ]; then + echo "Removing virtualenv ${VENV_DIR}..." + rm -Rf ${VENV_DIR} + fi +fi + +if [ ! -d ${VENV_DIR} ]; then + echo "Building virtualenv..." + virtualenv ${VENV_DIR} + + INITIALIZE_VENV=1 +fi + +source ${VENV_DIR}/bin/activate + +if [ -v INITIALIZE_VENV ]; then + pip install -r requirements.txt +fi + +echo "Starting new instances..." +python wsgiserver.py diff --git a/examples/python-wsgi/pywsgiref_stop.sh b/examples/python-wsgi/pywsgiref_stop.sh new file mode 100755 index 0000000..a98315e --- /dev/null +++ b/examples/python-wsgi/pywsgiref_stop.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Stopping existing instances..." +kill -3 `ps -Aef | grep wsgiserver.py | grep -v grep | tr -s ' ' ';' | cut -f 2 -d ';'` diff --git a/examples/python-wsgi/requirements.txt b/examples/python-wsgi/requirements.txt new file mode 100644 index 0000000..9bff157 --- /dev/null +++ b/examples/python-wsgi/requirements.txt @@ -0,0 +1,2 @@ +-e ../../ +-e git+https://github.com/TestInABox/stackInABox#egg=stackinabox-0.10a diff --git a/examples/python-wsgi/wsgiserver.py b/examples/python-wsgi/wsgiserver.py new file mode 100644 index 0000000..5afa24a --- /dev/null +++ b/examples/python-wsgi/wsgiserver.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import + +from wsgiref.simple_server import make_server + +import app + + +httpd = make_server('localhost', 8081, app.app) +httpd.serve_forever() diff --git a/examples/uwsgi/app.py b/examples/uwsgi/app.py index e2d6063..d675173 100644 --- a/examples/uwsgi/app.py +++ b/examples/uwsgi/app.py @@ -1,10 +1,17 @@ """ Gunicorn Example App """ +import logging from stackinabox.services.hello import HelloService from stackinawsgi import App -app = App([HelloService()]) -app.StackInABoxUriUpdate('localhost:8081') +lf = logging.FileHandler('stackinawsgi.log') +lf.setLevel(logging.DEBUG) +log = logging.getLogger() +log.addHandler(lf) +log.setLevel(logging.DEBUG) + +app = App([HelloService]) +app.StackInABoxUriUpdate('http://localhost:8081') diff --git a/examples/uwsgi/uwsgi_app.ini b/examples/uwsgi/uwsgi_app.ini index b56f3bd..88bd430 100644 --- a/examples/uwsgi/uwsgi_app.ini +++ b/examples/uwsgi/uwsgi_app.ini @@ -7,8 +7,12 @@ lazy = 1 memory-report = 1 need-app = 1 +; Note: Until Issue #11 (Session Bug) is fully resolved, +; Stack-In-A-WSGI only supports a single worker, +; each worker will have its own session data completely +; independent of all other workers. [app] http-socket = 127.0.0.1:8081 -processes = 5 +processes = 1 module = app:app master = 1 diff --git a/examples/uwsgi/uwsgi_start.sh b/examples/uwsgi/uwsgi_start.sh index 9a7668d..6e53e51 100755 --- a/examples/uwsgi/uwsgi_start.sh +++ b/examples/uwsgi/uwsgi_start.sh @@ -2,13 +2,40 @@ VENV_DIR="uwsgi_example_app" +for ARG in ${@} +do + echo "Found argument: ${ARG}" + if [ "${ARG}" == "--reset" ]; then + echo " User requested virtualenv reset, long argument name" + let -i RESET_VENV=1 + elif ["${ARG}" == "-r" ]; then + echo " User requested virtualenv reset, short argument name" + let -i RESET_VENV=2 + fi +done + +MD5SUM_ROOT=`ls / | md5sum | cut -f 1 -d ' '` +MD5SUM_VENV=`ls ${VENV_DIR} | md5sum | cut -f 1 -d ' '` +if [ "${MD5SUM_ROOT}" == "${MD5SUM_VENV}" ]; then + echo "Virtual Environment target is root. Configuration not supported." + exit 1 +fi + +if [ -v RESET_VENV ]; then + echo "Checking for existing virtualenv to remove..." + if [ -d ${VENV_DIR} ]; then + echo "Removing virtualenv ${VENV_DIR}..." + rm -Rf ${VENV_DIR} + fi +fi + if [ ! -d ${VENV_DIR} ]; then + echo "Building virtualenv..." virtualenv ${VENV_DIR} INITIALIZE_VENV=1 fi - source ${VENV_DIR}/bin/activate if [ -v INITIALIZE_VENV ]; then diff --git a/stackinawsgi/test/test_wsgi_app.py b/stackinawsgi/test/test_wsgi_app.py index 8c161a3..34683f7 100644 --- a/stackinawsgi/test/test_wsgi_app.py +++ b/stackinawsgi/test/test_wsgi_app.py @@ -5,6 +5,8 @@ import unittest +import ddt + from stackinabox.services.hello import HelloService from stackinabox.stack import StackInABox @@ -18,6 +20,7 @@ ) +@ddt.ddt class TestWsgiApp(unittest.TestCase): """ Stack-In-A-WSGI's wsgi.app.App test suite @@ -200,5 +203,25 @@ def test_handle_as_callable(self): wsgi_mock = WsgiMock() response_body = ''.join(the_app(environment, wsgi_mock)) - self.assertEqual(wsgi_mock.status, '200') + self.assertEqual(wsgi_mock.status, '200 OK') self.assertEqual(response_body, 'Hello') + + @ddt.data( + (160, "Unknown Informational Status"), + (260, "Unknown Success Status"), + (360, "Unknown Redirection Status"), + (460, "Unknown Client Error"), + (560, "Unknown Server Error"), + (660, "Unknown Status") + ) + @ddt.unpack + def test_response_for_status(self, status, expected_value): + """ + Validate that the generic Unknown status text is returned for each + range of values. + """ + the_app = App([HelloService]) + self.assertEqual( + the_app.response_for_status(status), + expected_value + ) diff --git a/stackinawsgi/wsgi/app.py b/stackinawsgi/wsgi/app.py index 59fa313..6b64814 100644 --- a/stackinawsgi/wsgi/app.py +++ b/stackinawsgi/wsgi/app.py @@ -24,6 +24,110 @@ class App(object): A WSGI Application for running StackInABox under a WSGI host """ + # List of well-known status codes + status_values = { + # Official Status Codes + 100: "Continue", + 101: "Switching Protocols", + 102: "Processing", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 207: "Multi-Status Response", + 208: "Already Reported", + 226: "IM Used", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 306: "Switch Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Payload Too Large", + 414: "URI Too Long", + 415: "Unsupported Media Type", + 416: "Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Requested Header Fields Too Large", + 451: "Unavailable for Legal Reasons", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", + + # Unofficial Status Codes: + 103: "Checkpoint", + 420: "Method Failure", + 450: "Blocked by Windows Parental Control (MS)", + 498: "Invalid Token", + 499: "Token Required", + 509: "Bandwidth Limit Exceeded", + 530: "Site Frozen", + 440: "Login Timeout", + 449: "Retry With", + # 451 - Redirect (re-defined) + + 444: "No Response", + 495: "SSL Certificate Error", + 496: "SSL Certificate Required", + 497: "HTTP Request Sent to HTTPS Port", + 499: "Client Closed Request", + + 520: "Unknown Error", + 521: "Web Server Is Down", + 522: "Connection Timed Out", + 523: "Origin Is Unreachable", + 524: "A Timeout Occurred", + 525: "SSL Handshake Failed", + 526: "Invalid SSL Certificate", + + # The below codes are specific cases for the infrastructure + # supported here and should not conflict with anything above. + + # StackInABox Status Codes + 595: "Route Not Handled", + 596: "Unhandled Exception", + 597: "URI Is For Service That Is Unknown", + + # StackInAWSGI Status Codes + 593: "Session ID Missing from URI", + 594: "Invalid Session ID" + } + def __init__(self, services=None): """ Create the WSGI Application @@ -138,6 +242,34 @@ def CallStackInABox(self, request, response): result[2] ) + def response_for_status(cls, status): + """ + Generate a status string for the status code + + :param int status: the status code to look-up + :returns: string for the value or an appropriate Unknown value + """ + if status in cls.status_values: + return cls.status_values[status] + + elif status >= 100 and status < 200: + return "Unknown Informational Status" + + elif status >= 200 and status < 300: + return "Unknown Success Status" + + elif status >= 300 and status < 400: + return "Unknown Redirection Status" + + elif status >= 400 and status < 500: + return "Unknown Client Error" + + elif status >= 500 and status < 600: + return "Unknown Server Error" + + else: + return "Unknown Status" + def __call__(self, environ, start_response): """ Callable entry per the PEP-3333 WSGI spec @@ -153,7 +285,12 @@ def __call__(self, environ, start_response): response = Response() self.CallStackInABox(request, response) start_response( - str(response.status), + "{0} {1}".format( + response.status, + self.response_for_status( + response.status + ) + ), [(k, v) for k, v in response.headers.items()] ) yield response.body