├── test
├── .keep
├── test_app
│ ├── foo.html
│ ├── foo.txt
│ └── test_static.py
├── test_issue
│ ├── scaffold.py
│ ├── test_160.py
│ └── test_161.py
├── test_core
│ ├── test_application.py
│ ├── test_util.py
│ ├── test_views.py
│ ├── test_context.py
│ └── test_dispatch.py
├── crudlike.py
├── test_crudlike.py
├── test_extension
│ ├── test_analytics.py
│ ├── test_local.py
│ ├── test_debug.py
│ ├── test_annotation.py
│ ├── test_base.py
│ └── test_args.py
├── sample.py
└── test_object_dispatch.py
├── .coveragerc
├── MANIFEST.in
├── bench
├── ftest.py
├── ttest.py
└── ptest.py
├── example
├── future
│ ├── nothingbut.yaml
│ ├── controller.yaml
│ ├── wsgi2.py
│ ├── traverse.py
│ ├── routing.py
│ ├── methods.py
│ ├── restful.py
│ ├── multihead.py
│ └── monster.py
├── exception.py
├── hello.py
├── debugger.py
├── template.py
├── basic.py
├── template.html
├── stream2.py
├── stream.py
├── annotation.py
└── controller.py
├── web
├── core
│ ├── __init__.py
│ ├── release.py
│ ├── compat.py
│ ├── util.py
│ ├── context.py
│ ├── dispatch.py
│ ├── extension.py
│ └── view.py
├── server
│ ├── gevent_.py
│ ├── diesel_.py
│ ├── eventlet_.py
│ ├── appengine.py
│ ├── waitress_.py
│ ├── cherrypy_.py
│ ├── fcgi.py
│ ├── tornado_.py
│ └── stdlib.py
├── ext
│ ├── extmime.py
│ ├── analytics.py
│ ├── debug.py
│ ├── local.py
│ ├── serialize.py
│ ├── annotation.py
│ ├── base.py
│ └── args.py
└── app
│ └── static.py
├── .gitignore
├── docs
├── old
│ ├── recipes
│ │ ├── middleware.py
│ │ ├── response_registry.py
│ │ └── extend_template_namespace.py
│ └── flow.textile
├── app
│ ├── __init__.html
│ └── dycco.css
├── ext
│ └── __init__.html
├── server
│ ├── __init__.html
│ ├── appengine.html
│ ├── gevent_.html
│ ├── diesel_.html
│ ├── eventlet_.html
│ ├── waitress_.html
│ ├── fcgi.html
│ ├── cherrypy_.html
│ └── tornado_.html
├── example
│ ├── exception.html
│ ├── debugger.html
│ ├── basic.html
│ ├── hello.html
│ └── template.html
├── index.html
└── core
│ ├── __init__.html
│ └── compat.html
├── .travis.yml
├── setup.cfg
├── Makefile
├── LICENSE.txt
└── .pre-commit-config.yaml
/test/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/test_app/foo.html:
--------------------------------------------------------------------------------
1 | html
2 |
--------------------------------------------------------------------------------
/test/test_app/foo.txt:
--------------------------------------------------------------------------------
1 | text
2 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit = web/server/*, web/core/release.py
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | global-include *.py *.txt *.rst
2 | global-exclude *.pyc *.pyo .gitignore
3 |
4 | prune .git
5 | prune htmlcov
6 | prune docs
7 | prune bench
8 | prune example
9 | prune test
10 |
--------------------------------------------------------------------------------
/bench/ftest.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | app = Flask(__name__)
3 |
4 | @app.route("/")
5 | def hello():
6 | return "Hello World!"
7 |
8 | if __name__ == "__main__":
9 | app.run()
10 |
--------------------------------------------------------------------------------
/test/test_issue/scaffold.py:
--------------------------------------------------------------------------------
1 | from web.core.application import Application
2 | from web.ext.debug import DebugExtension
3 |
4 | def serve(root):
5 | Application(root, extensions=[DebugExtension()]).serve('waitress')
6 |
--------------------------------------------------------------------------------
/test/test_issue/test_160.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | class Root:
4 | def __init__(self, context):
5 | self._ctx = context
6 |
7 | if __name__ == '__main__':
8 | __import__('scaffold').serve(Root)
9 | # GET /test - Should 404.
10 |
--------------------------------------------------------------------------------
/example/future/nothingbut.yaml:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env web
2 |
3 | version: 1
4 |
5 | exec:
6 | command: serve
7 |
8 | server:
9 | use: web.cli.serve.service:Marrow
10 | host: # bind to all interfaces
11 | port: 8080
12 |
13 | application:
14 | use: web.core.application:Application
15 | root: "Hello world."
16 |
--------------------------------------------------------------------------------
/test/test_issue/test_161.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | class Root:
4 | def __init__(self, context):
5 | self._ctx = context
6 |
7 | def test(self, *args):
8 | assert not args
9 |
10 | if __name__ == '__main__':
11 | __import__('scaffold').serve(Root)
12 | # GET /test - Should not drop into debugger. (Args should be empty.)
13 |
--------------------------------------------------------------------------------
/bench/ttest.py:
--------------------------------------------------------------------------------
1 | import tornado.ioloop
2 | import tornado.web
3 |
4 | class MainHandler(tornado.web.RequestHandler):
5 | def get(self):
6 | self.write("Hello, world")
7 |
8 | application = tornado.web.Application([
9 | (r"/", MainHandler),
10 | ])
11 |
12 | if __name__ == "__main__":
13 | application.listen(8888)
14 | tornado.ioloop.IOLoop.instance().start()
15 |
--------------------------------------------------------------------------------
/example/exception.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Exception handling test application.
4 |
5 | This application always raises 404 Not Found.
6 | """
7 |
8 | from webob.exc import HTTPNotFound
9 |
10 |
11 | def exception(context):
12 | raise HTTPNotFound()
13 |
14 |
15 | if __name__ == '__main__':
16 | from web.core import Application
17 | Application(exception).serve('wsgiref')
18 |
19 |
--------------------------------------------------------------------------------
/example/hello.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """A callable class example."""
4 |
5 |
6 | class Root(object):
7 | def __init__(self, context):
8 | self._ctx = context
9 |
10 | def __call__(self, name):
11 | """
12 | / -- 500
13 | /?name=Bob
14 | / POST name=bob
15 | /Bob
16 | """
17 | return "Hello " + name
18 |
19 |
20 | if __name__ == '__main__':
21 | from web.core import Application
22 | Application(Root).serve('wsgiref')
23 |
24 |
--------------------------------------------------------------------------------
/example/future/controller.yaml:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env web
2 |
3 | version: 1
4 |
5 | exec:
6 | command: serve
7 |
8 | server:
9 | use: web.cli.serve.service:Marrow
10 | host: # bind to all interfaces
11 | port: 8080
12 |
13 | application:
14 | use: web.core.application:Application
15 | root: !!python/name:examples.controller.Root
16 |
17 | extensions:
18 | - use: web.ext.template:TemplateExtension
19 | default: mako
20 |
--------------------------------------------------------------------------------
/test/test_core/test_application.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 | from unittest import TestCase
6 | from inspect import isclass
7 |
8 | from web.core.application import Application
9 |
10 |
11 | class TestApplicationParts(TestCase):
12 | def setUp(self):
13 | self.app = Application("Hi.")
14 |
15 | def test_application_attributes(self):
16 | assert isclass(self.app.RequestContext), "Non-class prepared request context."
17 |
18 |
--------------------------------------------------------------------------------
/example/debugger.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """A one-function WebCore 2 demonstration application.
4 |
5 | Applications can be as simple or as complex and layered as your needs dictate.
6 | """
7 |
8 |
9 | def basic(context, name="world"):
10 | 1/0
11 | return "Hello {name}.".format(name=name)
12 |
13 |
14 | if __name__ == '__main__':
15 | from web.core import Application
16 | from web.ext.debug import DebugExtension
17 |
18 | Application(basic, extensions=[DebugExtension()]).serve('waitress')
19 |
20 |
--------------------------------------------------------------------------------
/web/core/__init__.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | # ## Imports
4 |
5 | from threading import local as __local
6 |
7 | # Expose these as importable from the top-level `web.core` namespace.
8 |
9 | from .application import Application
10 | from .util import lazy
11 |
12 |
13 | # ## Module Globals
14 |
15 | __all__ = ['local', 'Application', 'lazy'] # Symbols exported by this package.
16 |
17 | # This is to support the web.ext.local extension, and allow for early importing of the variable.
18 | local = __local()
19 |
20 |
--------------------------------------------------------------------------------
/web/server/gevent_.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Gevent-based WSGI server adapter."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals, print_function
8 |
9 | from gevent.pywsgi import WSGIServer
10 |
11 |
12 | # ## Server Adapter
13 |
14 | def serve(application, host='127.0.0.1', port=8080):
15 | """Gevent-based WSGI-HTTP server."""
16 |
17 | # Instantiate the server with a host/port configuration and our application.
18 | WSGIServer((host, int(port)), application).serve_forever()
19 |
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python Bytecode
2 | *.py[cod]
3 |
4 | # C Extensions
5 | *.so
6 |
7 | # Packaging
8 | *.egg-info
9 | .eggs
10 | .packaging
11 |
12 | # Virtual Environment Pseudo-Chroot
13 | bin
14 | include
15 | lib
16 | share
17 | tmp
18 | usr
19 | var
20 |
21 | # Unit Test / Coverage Reports
22 | .cache
23 | .cagoule.db
24 | .coverage
25 | .tox
26 | .pytest_cache
27 | coverage.xml
28 | htmlcov
29 |
30 | # Translations
31 | *.mo
32 | *.pot
33 |
34 | # Mac
35 | .DS_Store
36 |
37 | # Completion Integration
38 | .ropeproject
39 | tags
40 |
41 |
--------------------------------------------------------------------------------
/bench/ptest.py:
--------------------------------------------------------------------------------
1 | from wsgiref.simple_server import make_server
2 | from pyramid.config import Configurator
3 | from pyramid.response import Response
4 | from waitress import serve as serve_
5 |
6 | def hello(request):
7 | return Response('Hello %(name)s!' % request.matchdict)
8 |
9 | if __name__ == '__main__':
10 | config = Configurator()
11 | config.add_route('hello', '/hello/{name}')
12 | config.add_view(hello, route_name='hello')
13 | app = config.make_wsgi_app()
14 | serve_(app, host='127.0.0.1', port=8080, threads=4)
15 |
16 |
--------------------------------------------------------------------------------
/web/ext/extmime.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from mimetypes import guess_type
4 |
5 |
6 | class AcceptFilenameExtension(object):
7 | """This pre-processes the incoming request URL, using the mimetype associated with the filename extension as the
8 | Accept header."""
9 |
10 | first = True
11 |
12 | needs = {'request'}
13 | provides = {'request.accept'}
14 |
15 | def prepare(self, context):
16 | encoding, compression = guess_type(context.environ['PATH_INFO'])
17 |
18 | if encoding:
19 | context.request.accept = encoding + context.request.accept
20 |
--------------------------------------------------------------------------------
/example/future/wsgi2.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Enhanced WSGI 2 (PEP 444) test application.
4 |
5 | The "enhanced" part is getting a copy of the context rather than request environ.
6 | """
7 |
8 |
9 | def enhanced(context):
10 | return b'200 OK', [(b'Content-Type', b'text/plain'), (b'Content-Length', b'12')], [b'Hello world.']
11 |
12 |
13 | if __name__ == '__main__':
14 | from web.core.application import Application
15 | from marrow.server.http import HTTPServer
16 |
17 | HTTPServer('127.0.0.1', 8080, application=Application(enhanced)).start()
18 |
--------------------------------------------------------------------------------
/example/future/traverse.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Basic traversal demonstration application.
4 |
5 | Applications can be as simple or as complex and layered as your needs dictate.
6 | """
7 |
8 |
9 | class Root(object):
10 | __dispatch__ = 'traversal'
11 |
12 | def __getitem__(self, name):
13 | return dict(about="About us.", contact="Contact information.")
14 |
15 |
16 | if __name__ == '__main__':
17 | from web.core.application import Application
18 | from marrow.server.http import HTTPServer
19 |
20 | HTTPServer('127.0.0.1', 8080, application=Application(Root)).start()
21 |
--------------------------------------------------------------------------------
/web/server/diesel_.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Diesel-based WSGI server adapter."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals, print_function
8 |
9 | from diesel.protocols.wsgi import WSGIApplication
10 |
11 |
12 | # ## Server Adapter
13 |
14 | def serve(application, host='127.0.0.1', port=8080):
15 | """Diesel-based (greenlet) WSGI-HTTP server.
16 |
17 | As a minor note, this is crazy. Diesel includes Flask, too.
18 | """
19 |
20 | # Instantiate the server with a host/port configuration and our application.
21 | WSGIApplication(application, port=int(port), iface=host).run()
22 |
23 |
--------------------------------------------------------------------------------
/web/server/eventlet_.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Eventlet-based WSGI server adapter."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals, print_function
8 |
9 | from eventlet import listen
10 | from eventlet.wsgi import server
11 |
12 |
13 | # ## Server Adapter
14 |
15 | def serve(application, host='127.0.0.1', port=8080):
16 | """Eventlet-based WSGI-HTTP server.
17 |
18 | For a more fully-featured Eventlet-capable interface, see also [Spawning](http://pypi.python.org/pypi/Spawning/).
19 | """
20 |
21 | # Instantiate the server with a bound port and with our application.
22 | server(listen(host, int(port)), application)
23 |
24 |
--------------------------------------------------------------------------------
/test/test_core/test_util.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from web.core.context import Context
4 | from web.core.util import lazy
5 |
6 | def mock_lazy_value(context):
7 | context.ran = True
8 | context.count += 1
9 | return 42
10 |
11 |
12 | def test_lazy_context_value():
13 | Ctx = Context(sample=lazy(mock_lazy_value, 'sample'))._promote('MockContext', False)
14 | ctx = Ctx(count=0, ran=False)
15 |
16 | assert isinstance(Ctx.sample, lazy)
17 | assert 'sample' not in ctx.__dict__
18 | assert not ctx.ran
19 | assert ctx.count == 0
20 | assert ctx.sample == 42
21 | assert 'sample' in ctx.__dict__
22 | assert ctx.sample == 42
23 | assert ctx.count == 1
24 |
25 |
--------------------------------------------------------------------------------
/example/template.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Template rendering sample application.
4 |
5 | This renders the test.html file contained in the current working directory.
6 | """
7 |
8 |
9 | def template(context):
10 | context.log.info("Returning template result.")
11 | return 'mako:./template.html', dict()
12 |
13 |
14 | if __name__ == '__main__':
15 | from web.core import Application
16 | from web.ext.template import TemplateExtension
17 |
18 | # Create the underlying WSGI application, passing the extensions to it.
19 | app = Application(template, extensions=[TemplateExtension()])
20 |
21 | # Start the development HTTP server.
22 | app.serve('wsgiref')
23 |
24 |
--------------------------------------------------------------------------------
/web/server/appengine.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Python-standard reference servers for development use."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | import warnings
10 |
11 | from google.appengine.ext.webapp.util import run_wsgi_app
12 |
13 |
14 | # ## Server Adapter
15 |
16 | def appengine(application):
17 | """Google App Engine adapter, CGI.
18 |
19 | Note: This adapter is essentially untested, and likely duplicates the `cgiref` adapter.
20 | """
21 |
22 | warnings.warn("Interactive debugging and other persistence-based processes will not work.")
23 |
24 | # Bridge the current CGI request.
25 | run_wsgi_app(application)
26 |
27 |
--------------------------------------------------------------------------------
/test/crudlike.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 |
6 | class Person(object):
7 | def __init__(self, username):
8 | self._username = username
9 |
10 | def __call__(self):
11 | return "Hi, I'm " + self._username
12 |
13 | def foo(self):
14 | return "I'm also " + self._username
15 |
16 |
17 | class People(object):
18 | def __init__(self, context=None):
19 | self._ctx = context
20 |
21 | def __call__(self):
22 | return "I'm all people."
23 |
24 | def __getattr__(self, username, **kw):
25 | return Person(username)
26 |
27 |
28 | class Root(object):
29 | def __init__(self, context=None):
30 | self._ctx = context
31 |
32 | user = People
33 |
--------------------------------------------------------------------------------
/docs/old/recipes/middleware.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Adding middleware to the stack is fairly straight-forward."""
4 |
5 |
6 | class MiddlewareExtension(object):
7 | uses = [] # add soft dependencies or omit
8 | needs = [] # add hard dependencies or omit
9 | always = True # usually you don't want to be required to activate your own extensions
10 | provides = ['sample'] # can be omitted if always is True
11 |
12 | def __init__(self, config):
13 | """Executed to configure the extension."""
14 | super(MiddlewareExtension, self).__init__()
15 | self.config = config
16 |
17 | def __call__(self, app):
18 | return MyMiddleware(app, self.config)
19 |
--------------------------------------------------------------------------------
/example/future/routing.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Basic route-based demonstration application.
4 |
5 | Applications can be as simple or as complex and layered as your needs dictate.
6 | """
7 |
8 | from web.dialect.route import route
9 |
10 |
11 | class Root(object):
12 | __dispatch__ = 'route'
13 |
14 | @route('/')
15 | def index(self):
16 | return "Root handler."
17 |
18 | @route('/page/{name}')
19 | def page(self, name):
20 | return "Page handler for: " + name
21 |
22 |
23 | if __name__ == '__main__':
24 | from web.core.application import Application
25 | from marrow.server.http import HTTPServer
26 |
27 | HTTPServer('127.0.0.1', 8080, application=Application(Root)).start()
28 |
--------------------------------------------------------------------------------
/example/basic.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """A one-function WebCore 2 demonstration application.
4 |
5 | Applications can be as simple or as complex and layered as your needs dictate.
6 | """
7 |
8 |
9 | def basic(context, name="world"):
10 | """Say hello.
11 |
12 | This can be tested easily using cURL from the command line:
13 |
14 | curl http://localhost:8080/ # Default value via GET.
15 | curl http://localhost:8080/Alice # Positionally specified via GET.
16 | curl -d name=Eve http://localhost:8080/ # Form-encoded value via POST.
17 | """
18 | return "Hello {name}.".format(name=name)
19 |
20 |
21 | if __name__ == '__main__':
22 | from web.core import Application
23 |
24 | Application(basic).serve('waitress', threads=16)
25 |
26 |
--------------------------------------------------------------------------------
/web/server/waitress_.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """The recommended development HTTP server."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals, print_function
8 |
9 | try:
10 | from waitress import serve as serve_
11 | except ImportError:
12 | print("You must install the 'waitress' package.")
13 | raise
14 |
15 |
16 | # ## Server Adapter
17 |
18 | def serve(application, host='127.0.0.1', port=8080, threads=4, **kw):
19 | """The recommended development HTTP server.
20 |
21 | Note that this server performs additional buffering and will not honour chunked encoding breaks.
22 | """
23 |
24 | # Bind and start the server; this is a blocking process.
25 | serve_(application, host=host, port=int(port), threads=int(threads), **kw)
26 |
27 |
--------------------------------------------------------------------------------
/example/future/methods.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Slightly more advanced example application.
4 |
5 | Non-functional until the helpers are worked on a bit more.
6 | """
7 |
8 | from web.dialect.helper import method
9 |
10 |
11 | class Root(object):
12 | def __init__(self, context):
13 | self._ctx = context
14 |
15 | @method.get
16 | def login(self):
17 | return "Present login form."
18 |
19 | @login.post
20 | def login(self, **data):
21 | # can call login.get() to explicitly call that handler.
22 | return "Actually log in."
23 |
24 |
25 | if __name__ == '__main__':
26 | from web.core.application import Application
27 | from marrow.server.http import HTTPServer
28 |
29 | HTTPServer('127.0.0.1', 8080, application=Application(Root)).start()
30 |
--------------------------------------------------------------------------------
/docs/old/recipes/response_registry.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Adding additional return value handlers is easy."""
4 |
5 | from web.core.response import register
6 |
7 |
8 | class MyObject(object):
9 | pass
10 |
11 |
12 | def handler(context, result):
13 | pass # do something to the context.response
14 |
15 |
16 | class SampleExtension(object):
17 | always = True # usually you don't want to be required to activate your own extensions
18 | provides = ['sample'] # can be omitted if always is True
19 |
20 | def __init__(self, config):
21 | """Executed to configure the extension."""
22 | super(SampleExtension, self).__init__()
23 |
24 | def start(self):
25 | # Register our custom handler; be sure to pick your type carefully!
26 | register(handler, MyObject)
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: false
3 | cache: pip
4 |
5 | branches:
6 | except:
7 | - /^[^/]+/.+$/
8 |
9 | python:
10 | - "2.7"
11 | - "pypy"
12 | - "pypy-5.7.1"
13 | - "3.4"
14 | - "3.5"
15 | - "3.6"
16 |
17 | install:
18 | - travis_retry pip install --upgrade setuptools pip setuptools_scm logilab-common
19 | - pip install -e '.[development]'
20 |
21 | script:
22 | pytest
23 |
24 | after_script:
25 | bash <(curl -s https://codecov.io/bash)
26 |
27 | notifications:
28 | irc:
29 | channels:
30 | - 'irc.freenode.org#webcore'
31 | use_notice: true
32 | skip_join: true
33 | on_success: change
34 | on_failure: always
35 | template:
36 | - "%{repository_slug}:%{branch}@%{commit} %{message}"
37 | - "Duration: %{duration} - Details: %{build_url}"
38 |
39 |
--------------------------------------------------------------------------------
/example/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | WebCore 2 Sample
4 |
10 |
11 |
12 | Welcome!
13 |
14 | This is an example WebCore 2 application.
15 |
16 |
17 | Context
18 |
19 | In no particular order, here is the public contents of the request-local context object:
20 |
21 |
22 | ${__import__('pprint').pformat(dict(web)) |h}
23 |
24 |
--------------------------------------------------------------------------------
/web/server/cherrypy_.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """CherryPy-based WSGI server adapter."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals, print_function
8 |
9 | from cherrypy.wsgiserver import CherryPyWSGIServer
10 |
11 |
12 | # ## Server Adapter
13 |
14 | def serve(application, host='127.0.0.1', port=8080):
15 | """CherryPy-based WSGI-HTTP server."""
16 |
17 | # Instantiate the server with our configuration and application.
18 | server = CherryPyWSGIServer((host, int(port)), application, server_name=host)
19 |
20 | # Try to be handy as many terminals allow clicking links.
21 | print("serving on http://{0}:{1}".format(host, port))
22 |
23 | # Bind and launch the server; this is a blocking operation.
24 | try:
25 | server.start()
26 | except KeyboardInterrupt:
27 | server.stop() # CherryPy has some of its own shutdown work to do.
28 |
29 |
--------------------------------------------------------------------------------
/web/server/fcgi.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """A production quality flup-based FastCGI server."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals, print_function
8 |
9 | try:
10 | from flup.server.fcgi import WSGIServer
11 | except ImportError:
12 | print("You must install a 'flup' package such as 'flup6' to use FastCGI support.")
13 | raise
14 |
15 |
16 | # ## Server Adapter
17 |
18 | def serve(application, host='127.0.0.1', port=8080, socket=None, **options):
19 | """Basic FastCGI support via flup.
20 |
21 | This web server has many, many options. Please see the Flup project documentation for details.
22 | """
23 |
24 | # Allow either on-disk socket (recommended) or TCP/IP socket use.
25 | if not socket:
26 | bindAddress = (host, int(port))
27 | else:
28 | bindAddress = socket
29 |
30 | # Bind and start the blocking web server interface.
31 | WSGIServer(application, bindAddress=bindAddress, **options).run()
32 |
33 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [aliases]
2 | test = pytest
3 |
4 | [check]
5 | metadata = 1
6 | restructuredtext = 1
7 |
8 | [clean]
9 | build-base = .packaging/build
10 | bdist-base = .packaging/dist
11 |
12 | [build]
13 | build-base = .packaging/build
14 |
15 | [install]
16 | optimize = 1
17 |
18 | [bdist]
19 | bdist-base = .packaging/dist
20 | dist-dir = .packaging/release
21 |
22 | [bdist_egg]
23 | bdist-dir = .packaging/dist
24 | dist-dir = .packaging/release
25 |
26 | [bdist_wheel]
27 | bdist-dir = .packaging/dist
28 | dist-dir = .packaging/release
29 |
30 | [register]
31 | ;repository = https://pypi.python.org/pypi
32 | strict = 1
33 |
34 | [upload]
35 | ;repository = https://pypi.python.org/pypi
36 | ;sign = 1
37 | ;identity = ...
38 |
39 | [tool:pytest]
40 | addopts =
41 | -l -r fEsxw
42 | --flakes
43 | --cov-report term-missing
44 | --cov-report xml
45 | --no-cov-on-fail
46 | --cov web
47 | --durations=5
48 | --color=yes
49 | test
50 |
51 | [wheel]
52 | universal = 1
53 |
54 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROJECT = WebCore
2 | USE = development,yaml,waitress,flup
3 |
4 | .PHONY: all develop clean veryclean test release
5 |
6 | all: clean develop test
7 |
8 | develop: ${PROJECT}.egg-info/PKG-INFO
9 |
10 | clean:
11 | find . -name __pycache__ -exec rm -rfv {} +
12 | find . -iname \*.pyc -exec rm -fv {} +
13 | find . -iname \*.pyo -exec rm -fv {} +
14 | rm -rvf build htmlcov
15 |
16 | veryclean: clean
17 | rm -rvf *.egg-info .packaging
18 |
19 | test: develop
20 | ./setup.py test
21 |
22 | release:
23 | ./setup.py register sdist bdist_wheel upload ${RELEASE_OPTIONS}
24 | @echo -e "\nView online at: https://pypi.python.org/pypi/${PROJECT} or https://pypi.org/project/${PROJECT}/"
25 | @echo -e "Remember to make a release announcement and upload contents of .packaging/release/ folder as a Release on GitHub.\n"
26 |
27 | ${PROJECT}.egg-info/PKG-INFO: setup.py setup.cfg web/core/release.py
28 | @mkdir -p ${VIRTUAL_ENV}/lib/pip-cache
29 | pip install --cache-dir "${VIRTUAL_ENV}/lib/pip-cache" -e ".[${USE}]"
30 |
31 |
--------------------------------------------------------------------------------
/test/test_crudlike.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 | import pytest
6 |
7 | from webob import Request
8 | from web.core import Application
9 |
10 | from crudlike import Root
11 |
12 |
13 | def do(path):
14 | app = Application(Root)
15 | req = Request.blank(path)
16 | return req.get_response(app)
17 |
18 |
19 | class TestCrudlikeRoot(object):
20 | def test_resolve_root(self):
21 | with pytest.raises(TypeError):
22 | do('/')
23 |
24 | def test_no_resolve_other(self):
25 | assert do('/foo').status_int == 404
26 |
27 | def test_no_resolve_private(self):
28 | assert do('/__class__').status_int == 404
29 |
30 |
31 | class TestCrudlikeCollection(object):
32 | def test_resolve_user_collection(self):
33 | assert do('/user').text == "I'm all people."
34 |
35 | def test_resolve_specific_user(self):
36 | assert do('/user/GothAlice').text == "Hi, I'm GothAlice"
37 |
38 | def test_resolve_specific_user_action(self):
39 | assert do('/user/GothAlice/foo').text == "I'm also GothAlice"
40 |
41 |
--------------------------------------------------------------------------------
/web/server/tornado_.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | # ## Imports
4 |
5 | from __future__ import unicode_literals, print_function
6 |
7 | try:
8 | import tornado.ioloop
9 | import tornado.httpserver
10 | import tornado.wsgi
11 | except ImportError:
12 | print("You must install the 'tornado' package.")
13 | raise
14 |
15 |
16 | # ## Server Adapter
17 |
18 | def serve(application, host='127.0.0.1', port=8080, **options):
19 | """Tornado's HTTPServer.
20 |
21 | This is a high quality asynchronous server with many options. For details, please visit:
22 |
23 | http://www.tornadoweb.org/en/stable/httpserver.html#http-server
24 | """
25 |
26 | # Wrap our our WSGI application (potentially stack) in a Tornado adapter.
27 | container = tornado.wsgi.WSGIContainer(application)
28 |
29 | # Spin up a Tornado HTTP server using this container.
30 | http_server = tornado.httpserver.HTTPServer(container, **options)
31 | http_server.listen(int(port), host)
32 |
33 | # Start and block on the Tornado IO loop.
34 | tornado.ioloop.IOLoop.instance().start()
35 |
36 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright © 2006-2019 Alice Bevan-McGregor and contributors.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/example/future/restful.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Slightly more advanced example application."""
4 |
5 |
6 | class Person:
7 | __dispatch__ = 'rest'
8 |
9 | def get(self):
10 | return "Person details."
11 |
12 | def post(self):
13 | # Often not supported!
14 | return "Create a child object."
15 |
16 | def put(self):
17 | return "Replace or create this person."
18 |
19 | def delete(self):
20 | return "Delete this person."
21 |
22 |
23 | class Root:
24 | __dispatch__ = 'rest'
25 | __resource__ = Person
26 |
27 | def get(self):
28 | return "List of people."
29 |
30 | def post(self):
31 | return "Create new person."
32 |
33 | def put(self):
34 | return "Replace all people."
35 |
36 | def delete(self):
37 | return "Delete all people."
38 |
39 |
40 |
41 | if __name__ == '__main__':
42 | from web.core.application import Application
43 | from marrow.server.http import HTTPServer
44 |
45 | HTTPServer('127.0.0.1', 8080, application=Application(Root)).start()
46 |
--------------------------------------------------------------------------------
/test/test_extension/test_analytics.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import time
4 | import pytest
5 |
6 | from webob import Request
7 | from web.core import Application
8 | from web.core.context import Context
9 | from web.ext.analytics import AnalyticsExtension
10 |
11 |
12 | def endpoint(context):
13 | time.sleep(0.1)
14 | return "Hi."
15 |
16 |
17 | sample = Application(endpoint, extensions=[AnalyticsExtension()])
18 |
19 |
20 | def test_analytics_extension():
21 | ctx = Context(response=Context(headers=dict()))
22 | ext = AnalyticsExtension()
23 |
24 | assert not hasattr(ctx, '_start_time')
25 | ext.prepare(ctx)
26 |
27 | assert hasattr(ctx, '_start_time')
28 | ext.before(ctx)
29 | time.sleep(0.1)
30 |
31 | ext.after(ctx)
32 | assert 100 <= float(ctx.response.headers['X-Generation-Time']) <= 200
33 |
34 |
35 | def test_analytics_extension_in_context():
36 | try:
37 | __import__('web.dispatch.object')
38 | except ImportError:
39 | pytest.skip("web.dispatch.object not installed")
40 |
41 | resp = Request.blank('/').get_response(sample)
42 | assert 100 <= float(resp.headers['X-Generation-Time']) <= 200
43 |
44 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | - repo: https://github.com/pre-commit/pre-commit-hooks.git
2 | sha: v0.9.2
3 | hooks:
4 | - id: check-added-large-files
5 | - id: check-ast
6 | - id: check-byte-order-marker
7 | - id: check-docstring-first
8 | - id: check-merge-conflict
9 | - id: check-symlinks
10 | - id: debug-statements
11 | - id: detect-private-key
12 | - id: end-of-file-fixer
13 | - id: forbid-new-submodules
14 | - id: check-json
15 | - id: check-xml
16 | - id: check-yaml
17 | - repo: https://github.com/Lucas-C/pre-commit-hooks-safety
18 | sha: v1.1.0
19 | hooks:
20 | - id: python-safety-dependencies-check
21 | - repo: local
22 | hooks:
23 | - id: python-bandit-vulnerability-check
24 | name: bandit
25 | entry: bandit
26 | args: [-lll, --recursive, marrow, web, test]
27 | language: system
28 | files: ''
29 | - repo: local
30 | hooks:
31 | - id: py.test
32 | name: py.test
33 | language: system
34 | entry: sh -c 'TEST_SKIP_CAPPED=1 py.test'
35 | files: ''
36 |
--------------------------------------------------------------------------------
/test/test_extension/test_local.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import web
4 | from web.core import local
5 | from web.core.context import Context
6 | from web.ext.local import ThreadLocalExtension
7 |
8 |
9 | def test_existing_thread_local_extension():
10 | ctx = Context()
11 | ext = ThreadLocalExtension()
12 |
13 | assert not hasattr(local, 'context')
14 | ext.start(ctx)
15 |
16 | assert local.context is ctx
17 |
18 | rctx = ctx._promote('RequestContext')
19 | ext.prepare(rctx)
20 |
21 | assert local.context is rctx
22 |
23 | ext.done(rctx)
24 | assert not hasattr(local, 'context')
25 |
26 | ext.stop(ctx)
27 |
28 |
29 | def test_new_thread_local_extension():
30 | ctx = Context()
31 | ext = ThreadLocalExtension('web:local')
32 |
33 | assert not hasattr(web, 'local')
34 |
35 | ext.start(ctx)
36 |
37 | local = web.local
38 |
39 | assert local.context is ctx
40 |
41 | rctx = ctx._promote('RequestContext')
42 | ext.prepare(rctx)
43 |
44 | assert local.context is rctx
45 |
46 | ext.done(rctx)
47 | assert not hasattr(local, 'context')
48 |
49 | ext.stop(ctx)
50 |
51 | assert not hasattr(web, 'local')
52 |
53 |
--------------------------------------------------------------------------------
/example/future/multihead.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Allow a single method to return different data formats.
4 |
5 | Non-functional until the helpers get worked on a bit.
6 | """
7 |
8 | from web.dialect.helper import render, require
9 |
10 |
11 | class Root(object):
12 | @render('mako:./multihead.html') # default if no extension
13 | @render('mako:./multihead.html', 'html') # /details.html
14 | @render('mako:./multihead.xml', 'xml') # /details.xml
15 | @render('json:') # /details.json
16 | @render('bencode:') # /details.bencode
17 | @render('yaml:') # /details.yaml
18 | def details(self):
19 | return dict(name="Bob", age=27)
20 |
21 | @require(False)
22 | def foo(self):
23 | return "We matched the predicate."
24 |
25 | @foo.require(True)
26 | def foo(self):
27 | return "We matched a different predicate."
28 |
29 | @foo.otherwise
30 | def foo(self):
31 | return "uh, wut?"
32 |
33 |
34 |
35 | if __name__ == '__main__':
36 | from web.core.application import Application
37 | from marrow.server.http import HTTPServer
38 |
39 | HTTPServer('127.0.0.1', 8080, application=Application(Root)).start()
40 |
--------------------------------------------------------------------------------
/web/core/release.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Release information about WebCore."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | import sys
10 | from collections import namedtuple
11 |
12 |
13 | # ## Module Globals
14 |
15 | version_info = namedtuple('version_info', ('major', 'minor', 'micro', 'releaselevel', 'serial'))(3, 0, 0, 'final', 1)
16 | version = ".".join([str(i) for i in version_info[:3]]) + ((version_info.releaselevel[0] + str(version_info.serial)) if version_info.releaselevel != 'final' else '')
17 |
18 | author = namedtuple('Author', ['name', 'email'])("Alice Bevan-McGregor", 'alice@gothcandy.com')
19 | description = "A powerful web development nanoframework so small it's not even a microframework."
20 | copyright = "2009-2019, Alice Bevan-McGregor and contributors"
21 | url = 'https://github.com/marrow/WebCore/'
22 | colophon = """Powered by:
23 | Python {0.major}.{0.minor}
24 | and WebCore {2} .""".format(sys.version_info, url, version)
25 |
--------------------------------------------------------------------------------
/test/sample.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Objects used to evaluate dispatch in tests."""
4 |
5 | from collections import deque
6 |
7 |
8 | def init(self, context=None):
9 | self._ctx = context
10 |
11 |
12 | def path(path):
13 | return deque(path.split('/')[1:])
14 |
15 |
16 | def function(context, *args):
17 | return 'function /' + '/'.join(args)
18 |
19 |
20 | class Simple:
21 | _protected = True
22 | __init__ = init
23 | static = "foo"
24 |
25 | class foo:
26 | __init__ = init
27 |
28 | class bar:
29 | __init__ = init
30 |
31 | def baz(self):
32 | return "baz"
33 |
34 |
35 | class CallableShallow:
36 | __init__ = init
37 |
38 | def __call__(self, *args):
39 | return '/' + '/'.join(args)
40 |
41 |
42 | class CallableDeep:
43 | __init__ = init
44 |
45 | class foo:
46 | __init__ = init
47 |
48 | class bar:
49 | __init__ = init
50 |
51 | def __call__(self, *extra):
52 | return '/' + '/'.join(('foo', 'bar') + extra)
53 |
54 |
55 | class CallableMixed:
56 | __init__ = init
57 |
58 | def __call__(self, *args):
59 | return '/' + '/'.join(args)
60 |
61 | class foo:
62 | __init__ = init
63 |
64 | class bar:
65 | __init__ = init
66 |
67 | def baz(self):
68 | return "baz"
69 |
70 |
--------------------------------------------------------------------------------
/web/core/compat.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Compatibility helpers to bridge the differences between Python 2 and Python 3.
4 |
5 | Similar in purpose to [`six`](https://warehouse.python.org/project/six/). Not generally intended to be used by
6 | third-party software, these are subject to change at any time. Only symbols exported via `__all__` are safe to use.
7 | """
8 |
9 | # ## Imports
10 |
11 | import sys
12 |
13 |
14 | # ## Module Exports
15 |
16 | __all__ = ['py3', 'pypy', 'unicode', 'str']
17 |
18 |
19 | # ## Version Detection
20 |
21 | py3 = sys.version_info > (3, )
22 | pypy = hasattr(sys, 'pypy_version_info')
23 |
24 |
25 | # ## Builtins Compatibility
26 |
27 | # Use of the `items` shortcut here must be very, very careful to only apply it to actual bare dictionaries.
28 |
29 | if py3:
30 | unicode = str
31 | str = bytes
32 | items = dict.items
33 | else:
34 | unicode = unicode
35 | str = str
36 | items = dict.iteritems
37 |
38 |
39 | # ## File-Like String Handling
40 |
41 | try:
42 | try:
43 | from cStringIO import StringIO
44 | except ImportError:
45 | from StringIO import StringIO
46 | except ImportError:
47 | from io import StringIO
48 |
49 |
50 | # ## Python Standard Library Backports
51 |
52 | try:
53 | from pathlib import PurePosixPath as Path
54 | except ImportError:
55 | from pathlib2 import PurePosixPath as Path
56 |
57 |
--------------------------------------------------------------------------------
/test/test_core/test_views.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 | from unittest import TestCase
6 | from webob import Request
7 | from web.core.context import Context
8 | from web.core.compat import unicode
9 | from web.core.view import WebViews
10 |
11 |
12 | class TestWebViews(TestCase):
13 | def setUp(self):
14 | self._ctx = Context(
15 | request = Request.blank('/foo/bar'),
16 | extension = Context(signal=Context(dispatch=[])),
17 | dispatched = False,
18 | callback = False,
19 | events = [],
20 | )
21 | self.view = self._ctx.view = WebViews(self._ctx)
22 | self._ctx.environ = self._ctx.request.environ
23 |
24 | def mock_view(self, context, value):
25 | context.value = value
26 |
27 | def test_registration(self):
28 | assert dict not in self.view._map
29 | self.view.register(dict, self.mock_view)
30 | assert dict in self.view._map
31 | assert self.view._map.get(dict) == self.mock_view
32 |
33 | def test_resolution(self):
34 | cb = self.mock_view
35 | self.view.register(unicode, cb)
36 | self.view.register(int, object)
37 | results = list(self.view("hi"))
38 | assert len(results) == 1
39 | assert results[0] is cb
40 |
41 | def test_repr(self):
42 | assert repr(self.view) == "WebViews(0)"
43 | self.view.register(dict, self.mock_view)
44 | assert repr(self.view) == "WebViews(1)"
45 |
46 |
--------------------------------------------------------------------------------
/example/stream2.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from webob import Response
4 | from random import randint
5 | from time import sleep
6 | from waitress import serve
7 | from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError
8 |
9 |
10 | def mul(x, y):
11 | sleep(0.125)
12 | return ('text/plain', "{0} * {1} = {2}\n".format(x, y, x * y))
13 |
14 |
15 | executor = ThreadPoolExecutor(10)
16 |
17 |
18 | def app(environ, start_response):
19 | """Multipart AJAX request example.
20 |
21 | See: http://test.getify.com/mpAjax/description.html
22 | """
23 |
24 | response = Response()
25 |
26 | parts = []
27 |
28 | for i in range(12):
29 | for j in range(12):
30 | parts.append(executor.submit(mul, i, j))
31 |
32 | def stream(parts, timeout=None):
33 | try:
34 | for future in as_completed(parts, timeout):
35 | mime, result = future.result()
36 | result = result.encode('utf8')
37 |
38 | yield "!!!!!!=_NextPart_{num}\nContent-Type: {mime}\nContent-Length: {length}\n\n".format(
39 | num = randint(100000000, 999999999),
40 | mime = mime,
41 | length = len(result)
42 | ).encode('utf8') + result
43 |
44 | except TimeoutError:
45 | for future in parts:
46 | future.cancel()
47 |
48 | response.content_length = None
49 | response.app_iter = stream(parts, 0.2)
50 |
51 | return response(environ, start_response)
52 |
53 |
54 | if __name__ == '__main__':
55 | serve(app, host='127.0.0.1', port=8080, threads=4)
56 |
--------------------------------------------------------------------------------
/docs/app/__init__.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | __init__.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | __init__.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 | __import__ ( 'pkg_resources' ) . declare_namespace ( __name__ ) # pragma: no cover
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Generated by Dycco .
40 | Last updated 18 Apr 2016 .
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/docs/ext/__init__.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | __init__.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | __init__.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 | __import__ ( 'pkg_resources' ) . declare_namespace ( __name__ ) # pragma: no cover
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Generated by Dycco .
40 | Last updated 25 Apr 2016 .
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/docs/server/__init__.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | __init__.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | __init__.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 | __import__ ( 'pkg_resources' ) . declare_namespace ( __name__ ) # pragma: no cover
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Generated by Dycco .
40 | Last updated 25 Apr 2016 .
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/test/test_app/test_static.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 | import os.path
6 | import pytest
7 |
8 | from webob.exc import HTTPForbidden, HTTPNotFound
9 | from web.core.context import Context
10 | from web.app.static import static
11 |
12 |
13 | HERE = os.path.dirname(__file__)
14 |
15 | class Sample(object):
16 | ep = static(HERE)
17 | epm = static(HERE, dict(html='mako'))
18 | dev = static('/dev')
19 | far = static(HERE, far=('txt'))
20 |
21 | sample = Sample()
22 |
23 |
24 | def test_base_path_policy():
25 | with pytest.raises(HTTPForbidden):
26 | sample.ep(None, '..', 'foo')
27 |
28 | with pytest.raises(HTTPForbidden):
29 | sample.ep(None, '/', 'foo')
30 |
31 |
32 | def test_non_extant():
33 | with pytest.raises(HTTPNotFound):
34 | sample.ep(None, 'noex')
35 |
36 |
37 | def test_non_file():
38 | with pytest.raises(HTTPForbidden):
39 | sample.ep(None)
40 |
41 |
42 | def test_mapping():
43 | template, data = sample.epm(None, 'foo.html')
44 |
45 | assert data == dict()
46 | assert template == 'mako:' + os.path.join(HERE, 'foo.html')
47 |
48 |
49 | def test_file():
50 | fh = sample.ep(None, 'foo.html')
51 | assert fh.read().strip() == b'html'
52 | fh.close()
53 |
54 | fh = sample.ep(None, 'foo.txt')
55 | assert fh.read().strip() == b'text'
56 | fh.close()
57 |
58 |
59 | def test_far_future_expires():
60 | context = Context(response=Context())
61 | sample.far(context, 'foo.txt')
62 | assert context.response.cache_expires == 60*60*24*365
63 |
64 |
65 |
--------------------------------------------------------------------------------
/example/stream.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from random import randint
4 | from time import sleep
5 | from concurrent.futures import ThreadPoolExecutor, as_completed, TimeoutError
6 |
7 |
8 | def mul(x, y):
9 | sleep(0.125)
10 | return ('text/plain', "{0} * {1} = {2}\n".format(x, y, x * y))
11 |
12 |
13 | executor = ThreadPoolExecutor(10)
14 |
15 |
16 | def root(context):
17 | """Multipart AJAX request example.
18 |
19 | See: http://test.getify.com/mpAjax/description.html
20 | """
21 |
22 | response = context.response
23 |
24 | parts = []
25 |
26 | for i in range(12):
27 | for j in range(12):
28 | parts.append(executor.submit(mul, i, j))
29 |
30 | def stream(parts, timeout=None):
31 | try:
32 | for future in as_completed(parts, timeout):
33 | mime, result = future.result()
34 | result = result.encode('utf8')
35 |
36 | yield "!!!!!!=_NextPart_{num}\nContent-Type: {mime}\nContent-Length: {length}\n\n".format(
37 | num = randint(100000000, 999999999),
38 | mime = mime,
39 | length = len(result)
40 | ).encode('utf8') + result
41 |
42 | except TimeoutError:
43 | for future in parts:
44 | future.cancel()
45 |
46 | response.content_length = None
47 | response.app_iter = stream(parts, 0.2)
48 |
49 | return response
50 |
51 |
52 | if __name__ == '__main__':
53 | from web.core import Application
54 |
55 | # wsgiref streams the chunks correctly, waitress buffers in 18000 byte chunks.
56 | Application(root, logging={'level': 'debug'}).serve('waitress', send_bytes=1)
57 |
58 |
--------------------------------------------------------------------------------
/docs/old/recipes/extend_template_namespace.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Extending the template engine variable namespace to always include certain values is quite easy.
4 |
5 | There are two places you can extend:
6 |
7 | * The 'web' variable can be extended by assigning to the request context.
8 | * Adding additonal top-level variables is accomplished by assigning to the 'namespace' attribute of the context.
9 |
10 | Note that both of these sources are omitted from serialized rendering. To add variables for serialization use the
11 | __after__ handler within your controllers. Pro tip:
12 |
13 | class MyController(object):
14 | def __init__(self, context):
15 | self.ctx = context
16 |
17 | def index(self):
18 | return 'json:', dict(hello="world")
19 |
20 | def __after__(self, result):
21 | result[1]['meaning'] = 42
22 | """
23 |
24 |
25 | class SampleExtension(object):
26 | uses = [] # add soft dependencies or omit
27 | needs = [] # add hard dependencies or omit
28 | always = True # usually you don't want to be required to activate your own extensions
29 | provides = ['sample'] # can be omitted if always is True
30 |
31 | def __init__(self, config):
32 | """Executed to configure the extension."""
33 | super(SampleExtension, self).__init__()
34 |
35 | def prepare(self, context):
36 | """Executed during request set-up."""
37 |
38 | # The request context is used as the 'web' template variable.
39 | context.foo = 27
40 |
41 | # To extend the top-level namespace for temlpates:
42 | context.namespace.bar = 42
43 |
--------------------------------------------------------------------------------
/test/test_extension/test_debug.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import pytest
4 |
5 | from webob import Request
6 | from webob.exc import HTTPNotFound
7 | from web.core import Application
8 | from web.core.context import Context
9 | from web.ext.debug import Console, DebugExtension
10 |
11 |
12 | def mock_app(environ, start_response):
13 | 1/0
14 |
15 |
16 | def mock_endpoint(context):
17 | 1/0
18 |
19 |
20 | def test_debug_extension_console():
21 | ext = DebugExtension()
22 | req = Request.blank('/__console__')
23 | ctx = Context()
24 | app = ext(ctx, mock_app)
25 |
26 | response = req.get_response(app)
27 |
28 | assert 'debugger.js' in response.text
29 |
30 |
31 | def test_debug_extension_catches():
32 | ext = DebugExtension()
33 | req = Request.blank('/')
34 | ctx = Context()
35 | app = ext(ctx, mock_app)
36 |
37 | response = req.get_response(app)
38 |
39 | assert 'CONSOLE_MODE = false' in response.text
40 | assert 'by zero' in response.text
41 |
42 |
43 | def test_inline_console():
44 | ctx = Context()
45 | ctx.request = Request.blank('/')
46 | ext = DebugExtension()
47 | ext(ctx, mock_app)
48 |
49 | con = Console(ctx)
50 |
51 | result = con()
52 | assert 'CONSOLE_MODE = true' in result.text
53 |
54 |
55 |
56 | def test_inline_console_disallowed():
57 | ctx = Context()
58 | ctx.request = Request.blank('/')
59 | con = Console(ctx)
60 |
61 | with pytest.raises(HTTPNotFound):
62 | con()
63 |
64 |
65 | def test_full_pipeline():
66 | app = Application(mock_endpoint, extensions=[DebugExtension()])
67 | req = Request.blank('/')
68 | response = req.get_response(app)
69 |
70 | assert response.status_int == 500
71 | assert 'by zero' in response.text
72 |
73 |
--------------------------------------------------------------------------------
/test/test_core/test_context.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import pytest
4 |
5 | from web.core.context import Context, ContextGroup
6 |
7 |
8 | class Thing(object):
9 | def __init__(self):
10 | self.__name__ = None
11 |
12 |
13 | def test_basic_context_operations():
14 | sample = Context(foo=1, bar=2, _baz=3)
15 |
16 | assert sorted(sample) == ['bar', 'foo']
17 | assert len(sample) == 2
18 | assert sample['foo'] == 1
19 |
20 | del sample['bar']
21 |
22 | with pytest.raises(KeyError):
23 | sample['bar']
24 |
25 | with pytest.raises(KeyError):
26 | del sample['bar']
27 |
28 |
29 | def test_context_group_basics():
30 | group = ContextGroup()
31 | assert repr(group) == "ContextGroup()"
32 | assert len(group) == 0
33 | assert list(group) == []
34 |
35 | thing = group.foo = Thing()
36 | assert repr(group) == "ContextGroup(foo)"
37 | assert len(group) == 1
38 | assert list(group) == ['foo']
39 | assert group.foo is thing
40 |
41 | thing = group['bar'] = Thing()
42 | assert repr(group) == "ContextGroup(bar, foo)"
43 | assert len(group) == 2
44 | assert set(group) == {'foo', 'bar'}
45 | assert group['bar'] is thing
46 |
47 | assert 'foo' in group
48 | del group.foo
49 | assert len(group) == 1
50 | assert set(group) == {'bar'}
51 |
52 | del group['bar']
53 | assert len(group) == 0
54 |
55 |
56 | def test_context_group_initial_arguments():
57 | group = ContextGroup(foo=Thing(), bar=Thing())
58 | assert repr(group) == "ContextGroup(bar, foo)"
59 | assert len(group) == 2
60 | assert set(group) == {'foo', 'bar'}
61 |
62 |
63 | def test_context_group_default():
64 | inner = ContextGroup()
65 | group = ContextGroup(inner)
66 |
67 | thing = group.foo = Thing()
68 | assert inner.foo is thing
69 | assert group.foo is thing
70 | del group.foo
71 |
72 | assert 'foo' not in inner, list(inner)
73 | assert 'foo' not in group, group.foo
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/web/ext/analytics.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Record basic performance statistics."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | import time
10 |
11 | from web.core.compat import unicode
12 |
13 |
14 | # ## Module Globals
15 |
16 | log = __import__('logging').getLogger(__name__)
17 |
18 |
19 | # ## Extension
20 |
21 | class AnalyticsExtension(object):
22 | """Record performance statistics about each request, and potentially a lot more.
23 |
24 | By default this extension adds a `X-Generation-Time` header to all responses and logs the generation time at the
25 | `debug` level. You can disable either by passing `header=None` or `level=None`, or specify an alternate logging
26 | level by passing in the name of the level.
27 | """
28 |
29 | __slots__ = ('header', 'log')
30 |
31 | first = True # We need this processing to happen as early as possible.
32 | provides = ['analytics'] # Expose this symbol for other extensions to depend upon.
33 |
34 | def __init__(self, header='X-Generation-Time', level='debug'):
35 | """Executed to configure the extension."""
36 |
37 | super(AnalyticsExtension, self).__init__()
38 |
39 | # Record settings.
40 | self.header = header
41 | self.log = getattr(log, level) if level else None
42 |
43 | # ### Request-Local Callabacks
44 |
45 | def prepare(self, context):
46 | """Executed during request set-up."""
47 |
48 | context._start_time = None
49 |
50 | def before(self, context):
51 | """Executed after all extension prepare methods have been called, prior to dispatch."""
52 |
53 | context._start_time = time.time()
54 |
55 | def after(self, context, exc=None):
56 | """Executed after dispatch has returned and the response populated, prior to anything being sent to the client."""
57 |
58 | duration = context._duration = round((time.time() - context._start_time) * 1000) # Convert to ms.
59 | delta = unicode(duration)
60 |
61 | # Default response augmentation.
62 | if self.header:
63 | context.response.headers[self.header] = delta
64 |
65 | if self.log:
66 | self.log("Response generated in " + delta + " seconds.", extra=dict(
67 | duration = duration,
68 | request = id(context)
69 | ))
70 |
71 |
--------------------------------------------------------------------------------
/web/server/stdlib.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Python-standard reference servers for development use."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals, print_function
8 |
9 | from wsgiref.handlers import CGIHandler
10 | from wsgiref.simple_server import make_server
11 |
12 |
13 | # ## Production Warning
14 |
15 | # We let people know it's a bad idea to use these in production.
16 | if not __debug__:
17 | import warnings
18 | warnings.warn("Use of standard library reference servers in production is discouraged.", RuntimeWarning)
19 |
20 |
21 | # ## Server Adapters
22 |
23 | def simple(application, host='127.0.0.1', port=8080):
24 | """Python-standard WSGI-HTTP server for testing purposes.
25 |
26 | The additional work performed here is to match the default startup output of "waitress".
27 |
28 | This is not a production quality interface and will be have badly under load.
29 | """
30 |
31 | # Try to be handy as many terminals allow clicking links.
32 | print("serving on http://{0}:{1}".format(host, port))
33 |
34 | # Bind and launch the server; this is a blocking operation.
35 | make_server(host, int(port), application).serve_forever()
36 |
37 |
38 | def cgi(application):
39 | """Python-standard WSGI-CGI server for testing purposes.
40 |
41 | This is not a production quality interface and will behave badly under load. Python-as-CGI is not a very good way
42 | to deploy any application. (Startup/shutdown on every request is a PHP problem.) This _can_ be useful as a
43 | diagnostic tool in development, however.
44 | """
45 |
46 | if not __debug__:
47 | warnings.warn("Interactive debugging and other persistence-based processes will not work.")
48 |
49 | # Instantiate the handler and begin bridging the application.
50 | CGIHandler().run(application)
51 |
52 |
53 | def iiscgi(application):
54 | """A specialized version of the reference WSGI-CGI server to adapt to Microsoft IIS quirks.
55 |
56 | This is not a production quality interface and will behave badly under load.
57 | """
58 | try:
59 | from wsgiref.handlers import IISCGIHandler
60 | except ImportError:
61 | print("Python 3.2 or newer is required.")
62 |
63 | if not __debug__:
64 | warnings.warn("Interactive debugging and other persistence-based processes will not work.")
65 |
66 | IISCGIHandler().run(application)
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/test/test_extension/test_annotation.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import pytest
4 |
5 | from web.core.compat import py3
6 | from web.core.context import Context
7 |
8 |
9 | pytestmark = pytest.mark.skipif(not py3, reason="Python 3 required for annotation support.")
10 | endpoint = None # Correct mistaken linter.
11 | Endpoint = None # Correct mistaken linter.
12 |
13 |
14 | if py3:
15 | from web.ext.annotation import AnnotationExtension
16 |
17 | # This trick hides the syntax error from Python 2.
18 | exec("def endpoint(a: int, b: int) -> 'int': return a * b")
19 | exec("class Endpoint:\n\tdef endpoint(a: int, b: int): return a * b")
20 |
21 |
22 | def bare_endpoint(a, b): return a * b
23 |
24 |
25 | def test_annotation_extension():
26 | ext = AnnotationExtension()
27 | ctx = Context()
28 | args = []
29 | kwargs = {'a': '27', 'b': '42'}
30 |
31 | ext.mutate(ctx, endpoint, args, kwargs)
32 |
33 | assert kwargs == {'a': 27, 'b': 42}
34 |
35 |
36 | def test_annotation_bare():
37 | ext = AnnotationExtension()
38 | ctx = Context()
39 | args = []
40 | kwargs = {'a': '27', 'b': '42'}
41 |
42 | ext.mutate(ctx, bare_endpoint, args, kwargs)
43 |
44 | assert kwargs == {'a': '27', 'b': '42'}
45 |
46 | assert ext.transform(ctx, bare_endpoint, None) is None
47 |
48 |
49 | def test_annotation_method():
50 | ext = AnnotationExtension()
51 | ctx = Context()
52 | args = []
53 | kwargs = {'a': '27', 'b': '42'}
54 |
55 | ext.mutate(ctx, Endpoint().endpoint, args, kwargs)
56 |
57 | assert kwargs == {'a': 27, 'b': 42}
58 |
59 |
60 | def test_annotation_positional():
61 | ext = AnnotationExtension()
62 | ctx = Context()
63 | args = ['27', '42']
64 | kwargs = {}
65 |
66 | ext.mutate(ctx, endpoint, args, kwargs)
67 |
68 | assert args == [27, 42]
69 | assert kwargs == {}
70 |
71 |
72 | def test_annotation_transformation():
73 | ext = AnnotationExtension()
74 | ctx = Context()
75 |
76 | result = ext.transform(ctx, endpoint, 1134)
77 |
78 | assert result == ('int', 1134)
79 |
80 |
81 | def test_annotation_failure():
82 | ext = AnnotationExtension()
83 | ctx = Context()
84 | args = []
85 | kwargs = {'a': 'xyzzy'}
86 |
87 | with pytest.raises(ValueError):
88 | ext.mutate(ctx, endpoint, args, kwargs)
89 |
90 | try:
91 | ext.mutate(ctx, endpoint, args, kwargs)
92 | except ValueError as e:
93 | s = str(e)
94 |
95 | assert 'xyzzy' in s
96 | assert "argument 'a'" in s
97 |
--------------------------------------------------------------------------------
/test/test_extension/test_base.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 | from webob import Request, Response
6 |
7 | from web.core.application import Application
8 | from web.core.util import safe_name
9 |
10 |
11 | def binary_endpoint(ctx): return b"Word."
12 |
13 | def string_endpoint(ctx): return "Hi."
14 |
15 | def empty_endpoint(ctx): return None
16 |
17 | def response_endpoint(ctx): return Response(text="Yo.")
18 |
19 | def binary_file_endpoint(ctx): return open('LICENSE.txt', 'rb')
20 |
21 | def generator_endpoint(ctx):
22 | yield b'foo'
23 | yield b'bar'
24 |
25 |
26 | class MockController(object):
27 | def __init__(self, context):
28 | self._ctx = context
29 |
30 | def here(self):
31 | return str(self._ctx.path.current)
32 |
33 | def paths(self):
34 | return ', '.join(str(i.path) for i in self._ctx.path)
35 |
36 | def handlers(self):
37 | return ', '.join(safe_name(i.handler) for i in self._ctx.path)
38 |
39 |
40 | class TestBreadcrumbPath(object):
41 | def test_here(self):
42 | app = Application(MockController)
43 | response = Request.blank('/here').get_response(app).text
44 | assert response == '/here'
45 |
46 | def test_breadcrumb_list_paths(self):
47 | app = Application(MockController)
48 | response = Request.blank('/paths').get_response(app).text
49 | assert response == "., /paths"
50 |
51 | def test_breadcrumb_list_handlers(self):
52 | app = Application(MockController)
53 | response = Request.blank('/handlers').get_response(app).text
54 | assert response == "test_base:MockController, test_base:MockController.handlers"
55 |
56 |
57 | class TestDefaultViews(object):
58 | def do(self, endpoint):
59 | app = Application(endpoint)
60 | return Request.blank('/').get_response(app)
61 |
62 | def test_binary(self):
63 | assert self.do(binary_endpoint).text == "Word."
64 |
65 | def test_string(self):
66 | assert self.do(string_endpoint).text == "Hi."
67 |
68 | def test_none(self):
69 | response = self.do(empty_endpoint)
70 | assert response.text == ""
71 | assert response.content_length == None # Actually blank responses have no length.
72 |
73 | def test_response(self):
74 | assert self.do(response_endpoint).text == "Yo."
75 |
76 | def test_file(self):
77 | assert '2016' in self.do(binary_file_endpoint).text
78 |
79 | def test_generator(self):
80 | assert 'foobar' in self.do(generator_endpoint).text
81 |
82 |
--------------------------------------------------------------------------------
/example/annotation.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Python 3 function annotation and AnnotationExtension example."""
4 |
5 | from json import dumps
6 |
7 |
8 | class Root:
9 | def __init__(self, context):
10 | self._ctx = context
11 |
12 | def tmpl(self) -> 'mako:./template.html':
13 | return dict()
14 |
15 | def mul(self, a: int = None, b: int = None) -> 'json':
16 | """Multiply two values together and return the result via JSON.
17 |
18 | Python 3 function annotations are used to ensure that the arguments are integers. This requires the
19 | functionality of `web.ext.annotation:AnnotationExtension`.
20 |
21 | There are several ways to execute this method:
22 |
23 | * POST http://localhost:8080/mul
24 | * GET http://localhost:8080/mul?a=27&b=42
25 | * GET http://localhost:8080/mul/27/42
26 |
27 | The latter relies on the fact we can't descend past a callable method so the remaining path elements are
28 | used as positional arguments, whereas the others rely on keyword argument assignment from a form-encoded
29 | request body or query string arguments. (Security note: any data in the request body takes presidence over
30 | query string arguments!)
31 |
32 | You can easily test these on the command line using cURL:
33 |
34 | curl http://localhost:8080/mul/27/42 # HTTP GET
35 |
36 | curl -d a=27 -d b=42 http://localhost:8080/mul # HTTP POST
37 | """
38 |
39 | if not a or not b:
40 | return dict(message="Pass arguments a and b to multiply them together!")
41 |
42 | return dict(answer=a * b)
43 |
44 |
45 | class SampleExtension:
46 | """Here's an example of how to catch an annotation like this as a view handler."""
47 |
48 | def start(self, context):
49 | context.view.register(tuple, self.render_json)
50 |
51 | def render_json(self, context, result):
52 | # Bail out if this isn't a 2-tuple, or isn't intended for JSON serialization.
53 | # This is an informal protocol shared with the more complete `web.template` package and possibly others.
54 | if len(result) != 2 or result[0] != 'json':
55 | return
56 |
57 | resp = context.response
58 | resp.content_type = 'application/json'
59 | resp.encoding = 'utf-8'
60 | resp.text = dumps(result[1])
61 |
62 | return True
63 |
64 |
65 | if __name__ == '__main__':
66 | from web.core import Application
67 | from web.ext.annotation import AnnotationExtension
68 |
69 | Application(Root, extensions=[SampleExtension(), AnnotationExtension()]).serve('wsgiref')
70 |
71 |
--------------------------------------------------------------------------------
/web/ext/debug.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Web-based REPL shell and interactive debugger extension."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | from webob.exc import HTTPNotFound
10 | from backlash import DebuggedApplication
11 |
12 |
13 | # ## Module Globals
14 |
15 | log = __import__('logging').getLogger(__name__)
16 |
17 |
18 | # ## Controller Endpoint Utility
19 |
20 | class Console(object):
21 | """Attach a console to your web application at an arbitrary location."""
22 |
23 | __slots__ = ('debugger', 'request')
24 |
25 | def __init__(self, context):
26 | self.debugger = context.get('debugger', None)
27 | self.request = context.request
28 |
29 | def __call__(self, *args, **kw):
30 | if not self.debugger:
31 | raise HTTPNotFound()
32 |
33 | return self.debugger.display_console(self.request)
34 |
35 |
36 | # ## Extension
37 |
38 | class DebugExtension(object):
39 | """Enable an interactive exception debugger and interactive console.
40 |
41 | Possible configuration includes:
42 |
43 | * `path` -- the path to the interactive console, defaults to: `/__console__`
44 | * `verbose` -- show ordinarily hidden stack frames, defaults to: `False`
45 | """
46 |
47 | __slots__ = ('path', 'verbose')
48 |
49 | provides = ['debugger', 'console']
50 |
51 | def __init__(self, path="/__console__", verbose=False):
52 | if __debug__:
53 | log.debug("Initializing debugger extension.")
54 |
55 | self.path = path
56 | self.verbose = verbose
57 |
58 | super(DebugExtension, self).__init__()
59 |
60 | def init_console(self):
61 | """Add variables to the console context."""
62 | return dict()
63 |
64 | def init_debugger(self, environ):
65 | """Add variables to the debugger context."""
66 | return dict(context=environ.get('context'))
67 |
68 | def __call__(self, context, app):
69 | """Executed to wrap the application in middleware.
70 |
71 | The first argument is the application context, not request context.
72 |
73 | Accepts a WSGI application as the second argument and must likewise return a WSGI app.
74 | """
75 |
76 | if __debug__:
77 | log.debug("Wrapping application in debugger middleware.")
78 |
79 | app = DebuggedApplication(
80 | app,
81 | evalex = __debug__, # In production mode, this is a security no-no.
82 | show_hidden_frames = self.verbose,
83 | console_init_func = self.init_console,
84 | context_injectors = [self.init_debugger],
85 | )
86 |
87 | context.debugger = app
88 |
89 | return app
90 |
--------------------------------------------------------------------------------
/test/test_extension/test_args.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 | import json
6 | from unittest import TestCase
7 | from webob import Request
8 | from webob.exc import HTTPNotModified
9 |
10 | from web.core.application import Application
11 |
12 |
13 | class MockController(object):
14 | def __init__(self, context):
15 | self._ctx = context
16 |
17 | def endpoint(self, a, b):
18 | return str(int(a) * int(b))
19 |
20 | def sum(self, v):
21 | return str(sum(int(i) for i in v))
22 |
23 | def notmod(self):
24 | raise HTTPNotModified()
25 |
26 | def rich(self, data=None):
27 | if not data:
28 | return
29 |
30 | return json.dumps({'result': data})
31 |
32 |
33 | class TestArgumentAndExceptionHandling(TestCase):
34 | def do(self, path, **data):
35 | app = Application(MockController)
36 | req = Request.blank(path)
37 | if data:
38 | req.content_type = 'application/json'
39 | data.pop('_remove', None)
40 | if data:
41 | req.json = data
42 | return req.get_response(app)
43 |
44 | def test_positional(self):
45 | assert self.do('/endpoint/4/8').text == "32"
46 |
47 | def test_keyword(self):
48 | assert self.do('/endpoint?a=2&b=12').text == "24"
49 |
50 | def test_repeated(self):
51 | assert self.do('/sum?v=1&v=1&v=2&v=3&v=5').text == "12"
52 |
53 | def test_httpexception_catch(self):
54 | assert self.do('/notmod').status_int == 304
55 |
56 | def test_catch_mismatch(self):
57 | assert self.do('/endpoint/4/3/9').status_int == 404
58 |
59 | def test_array_basic(self):
60 | assert self.do('/rich?data=1&data=2').json['result'] == ['1', '2']
61 |
62 | def test_explicit_array(self):
63 | assert self.do('/rich?data[]=1').json['result'] == ['1']
64 | assert self.do('/rich?data[]=1&data[]=2').json['result'] == ['1', '2']
65 |
66 | def test_indexed_array(self):
67 | assert self.do('/rich?data.3=3&data.1=1&data.2=2').json['result'] == ['1', '2', '3']
68 |
69 | def test_dictionary(self):
70 | assert self.do('/rich?data.bar=1&data.baz=2').json['result'] == {'bar': '1', 'baz': '2'}
71 |
72 | def test_json_body(self):
73 | assert self.do('/rich', data={'foo': 1, 'bar': 2}).json['result'] == {'foo': 1, 'bar': 2}
74 |
75 | def test_empty_body(self):
76 | assert self.do('/rich', _remove=True).text == ""
77 |
78 | def test_trailing_slash_eliding(self):
79 | res = self.do('/endpoint/2/4/')
80 | assert res.status_int == 200
81 | assert res.text == "8"
82 |
83 | def test_mid_slash_eliding(self):
84 | res = self.do('/endpoint///2//4/')
85 | assert res.status_int == 200
86 | assert res.text == "8"
87 |
--------------------------------------------------------------------------------
/web/ext/local.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | # ## Imports
4 |
5 | from __future__ import unicode_literals
6 |
7 | from threading import local
8 |
9 | from marrow.package.loader import traverse
10 |
11 |
12 | # ## Module Globals
13 |
14 | # Standard logger object.
15 | log = __import__('logging').getLogger(__name__)
16 |
17 |
18 | # ## Extension
19 |
20 | class ThreadLocalExtension(object):
21 | """Provide the current context as a thread local global.
22 |
23 | This provides a convienent "superglobal" variable where you can store per-thread data.
24 |
25 | While the context itself is cleaned up after each call, any data you add won't be. These are not request-locals.
26 | """
27 |
28 | first = True
29 | provides = ['local', 'threadlocal']
30 |
31 | def __init__(self, where='web.core:local'):
32 | """Initialize thread local storage for the context.
33 |
34 | By default the `local` object in the `web.core` package will be populated as a `threading.local` pool. The
35 | context, during a request, can then be accessed as `web.core.local.context`. Your own extensions can add
36 | additional arbitrary data to this pool.
37 | """
38 |
39 | super(ThreadLocalExtension, self).__init__()
40 |
41 | if __debug__:
42 | log.debug("Initalizing ThreadLocal extension.")
43 |
44 | self.where = where
45 | self.local = None
46 | self.preserve = False
47 |
48 | def _lookup(self):
49 | module, _, name = self.where.rpartition(':')
50 | module = traverse(__import__(module), '.'.join(module.split('.')[1:]), separator='.')
51 |
52 | return module, name
53 |
54 | def start(self, context):
55 | module, name = self._lookup()
56 |
57 | if __debug__:
58 | log.debug("Preparing thread local storage and assigning main thread application context.")
59 |
60 | if hasattr(module, name):
61 | self.local = getattr(module, name)
62 | self.preserve = True
63 | else:
64 | self.local = local()
65 | setattr(module, name, self.local)
66 |
67 | self.local.context = context # Main thread application context.
68 |
69 | def stop(self, context):
70 | self.local = None
71 |
72 | if __debug__:
73 | log.debug("Cleaning up thread local storage.")
74 |
75 | if not self.preserve:
76 | module, name = self._lookup()
77 | delattr(module, name)
78 |
79 | def prepare(self, context):
80 | """Executed prior to processing a request."""
81 | if __debug__:
82 | log.debug("Assigning thread local request context.")
83 |
84 | self.local.context = context
85 |
86 | def done(self, result):
87 | """Executed after the entire response has been sent to the client."""
88 | if __debug__:
89 | log.debug("Cleaning up thread local request context.")
90 |
91 | del self.local.context
92 |
93 |
--------------------------------------------------------------------------------
/test/test_object_dispatch.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | from __future__ import unicode_literals
4 |
5 | import pytest
6 |
7 | from webob import Request
8 | from web.core import Application
9 |
10 | from sample import function, Simple, CallableShallow, CallableDeep, CallableMixed
11 |
12 |
13 | def do(root, path_):
14 | app = Application(root)
15 | req = Request.blank(path_)
16 | return req.get_response(app)
17 |
18 |
19 | class TestFunctionDispatch(object):
20 | def test_root_path_resolves_to_function(self):
21 | assert do(function, '/').text == 'function /'
22 |
23 | def test_deep_path_resolves_to_function(self):
24 | assert do(function, '/foo/bar/baz').text == 'function /foo/bar/baz'
25 |
26 |
27 | class TestSimpleDispatch(object):
28 | def test_protected_init_method(self):
29 | assert do(Simple, '/__init__').status_int == 404
30 |
31 | def test_protected_simple_value(self):
32 | assert do(Simple, '/_protected').status_int == 404
33 |
34 | def test_static_attribute(self):
35 | assert do(Simple, '/static').text == "foo"
36 |
37 | def test_shallow_class_lookup(self):
38 | with pytest.raises(TypeError):
39 | do(Simple, '/foo')
40 |
41 | def test_deep_class_lookup(self):
42 | with pytest.raises(TypeError):
43 | do(Simple, '/foo/bar')
44 |
45 | def test_partial_incomplete_lookup(self):
46 | assert do(Simple, '/foo/bar/diz').status_int == 404
47 |
48 | def test_deepest_endpoint_lookup(self):
49 | assert do(Simple, '/foo/bar/baz').text == "baz"
50 |
51 |
52 | class TestCallableShallowDispatch(object):
53 | def test_root_path_resolves_to_instance(self):
54 | assert do(CallableShallow, '/').text == "/"
55 |
56 | def test_deep_path_resolves_to_instance(self):
57 | assert do(CallableShallow, '/foo/bar/baz').text == "/foo/bar/baz"
58 |
59 |
60 | class TestCallableDeepDispatch(object):
61 | def test_shallow_class_lookup(self):
62 | with pytest.raises(TypeError):
63 | do(CallableDeep, '/foo')
64 |
65 | def test_deep_callable_class_lookup(self):
66 | assert do(CallableDeep, '/foo/bar').text == "/foo/bar"
67 |
68 | def test_incomplete_lookup(self):
69 | assert do(CallableDeep, '/foo/diz').status_int == 404
70 |
71 | def test_beyond_callable_class_lookup(self):
72 | assert do(CallableDeep, '/foo/bar/baz').text == "/foo/bar/baz"
73 |
74 |
75 | class TestCallableMixedDispatch(object):
76 | def test_callable_root(self):
77 | assert do(CallableMixed, '/').text == "/"
78 |
79 | def test_shallow_class_lookup(self):
80 | with pytest.raises(TypeError):
81 | do(CallableMixed, '/foo')
82 |
83 | def test_deep_callable_class_lookup(self):
84 | with pytest.raises(TypeError):
85 | do(CallableMixed, '/foo/bar')
86 |
87 | def test_incomplete_lookup(self):
88 | assert do(CallableMixed, '/foo/diz').status_int == 404
89 |
90 | def test_deepest_endpoint_lookup(self):
91 | assert do(CallableMixed, '/foo/bar/baz').text == "baz"
92 |
93 |
--------------------------------------------------------------------------------
/docs/old/flow.textile:
--------------------------------------------------------------------------------
1 | h1. WebCore 2 Application Flow
2 |
3 | h2. Startup
4 |
5 | # Application configuration is loaded from one of two sources:
6 | ## Directly passed-in dictionary configuration.
7 | ## Configuration file extended by command-line options.
8 | # A dependency graph of extensions is built. E.g.:
9 | ## Application needs @template@, @session@, and @db@.
10 | ## The @base@ extension is always active and always first.
11 | ## The session extension 'uses' the database extension if available.
12 | ## Final sorting: @['base', 'template', 'db', 'session']@
13 | # Each extension is initialized as encountered. This allows extensions to dynamically adjust their @needs@ and @uses@ based on configuration values.
14 | # Each extension, after dependency sorting, then has its @start@ method called.
15 |
16 |
17 | h2. Shutdown
18 |
19 | # Each extension, in reverse order, has its @stop@ method called.
20 |
21 |
22 | h2. Request
23 |
24 | # The request environment is captured by the WebCore WSGI Application.
25 | # A context object is prepared, containing:
26 | ## Global configuration in @config@.
27 | ## WSGI environment in @environ@.
28 | # Extension @prepare@ methods are called in order.
29 | ## The base extension adds @request@ and @response@ to the context.
30 | ## The template extension adds a @namespace@ Bunch and @render@ method to the context.
31 | ## The session extension adds a @session@ dict-like to the context.
32 | ## The database extension adds, depending on database engine, a @dbsession@ or @db@ object to the context.
33 | # Extension @before@ methods are called in reverse order.
34 | ## The database extension prepares a transaction, if appropriate.
35 | # Dispatch is called to route the request to the final controller.
36 | ## Controllers are instantiated with the context as the sole argument.
37 | # Extension @after@ methods are called in reverse order.
38 | ## The database extension conditionally commits the transaction.
39 | ## The session extension conditionally saves the updated session.
40 | # The WebCore WSGI application takes the @context.resopnse@ object and serves it to the client.
41 |
42 |
43 | h2. Optimizations
44 |
45 | The following are stored at application startup in order to eliminate the need to calculate them during a request.
46 |
47 | # @_ext@ is a dependency-sorted list of instantiated extension objects.
48 | # @_prepare@ is a sorted list of extension @prepare@ methods.
49 | # @_before@ is a sorted list of extension @before@ methods.
50 | # @_after@ is a reverse-sorted list of extension @after@ methods.
51 |
52 | In the avove example, these variables would contain:
53 |
54 | # @_ext = [BaseExtension(), TemplateExtension(), SessionExtension(), DatabaseExtension()]@
55 | # @_prepare = [BaseExtension.prepare, TemplateExtension.prepare, DatabaseExtension.prepare, SessionExtension.prepare]@
56 | # @_before = [DatabaseExtension.before]@
57 | # @_after = [SessionExtension.after, DatabaseExtension.after]@
58 |
--------------------------------------------------------------------------------
/docs/example/exception.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | exception.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | exception.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Exception handling test application.
29 | This application always raises 404 Not Found.
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
45 | from webob.exc import HTTPNotFound
46 |
47 |
48 |
49 |
50 |
51 |
52 |
55 |
56 |
57 |
58 | def exception ( context ):
59 | raise HTTPNotFound ()
60 |
61 |
62 | if __name__ == '__main__' :
63 | from web.core import Application
64 | Application ( exception ) . serve ( 'wsgiref' )
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | Generated by Dycco .
73 | Last updated 25 Apr 2016 .
74 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/web/app/static.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Basic static file delivery mechanism."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | from os.path import abspath, normpath, exists, isfile, join as pathjoin, basename
10 | from webob.exc import HTTPForbidden, HTTPNotFound
11 |
12 |
13 | # ## Module Globals
14 |
15 | # A standard logging object.
16 | log = __import__('logging').getLogger(__name__)
17 |
18 |
19 | # ## Static File Endpoint
20 |
21 | def static(base, mapping=None, far=('js', 'css', 'gif', 'jpg', 'jpeg', 'png', 'ttf', 'woff')):
22 | """Serve files from disk.
23 |
24 | This utility endpoint factory is meant primarily for use in development environments; in production environments
25 | it is better (more efficient, secure, etc.) to serve your static content using a front end load balancer such as
26 | Nginx.
27 |
28 | The first argument, `base`, represents the base path to serve files from. Paths below the attachment point for
29 | the generated endpoint will combine this base path with the remaining path elements to determine the file to
30 | serve.
31 |
32 | The second argument is an optional dictionary mapping filename extensions to template engines, for cooperation
33 | with the TemplateExtension. (See: https://github.com/marrow/template) The result of attempting to serve a
34 | mapped path is a 2-tuple of `("{mapping}:{path}", dict())`. For example, to render all `.html` files as Mako
35 | templates, you would attach something like the following:
36 |
37 | class Root:
38 | page = static('/path/to/static/pages', dict(html='mako'))
39 |
40 | By default the "usual culprits" are served with far-futures cache expiry headers. If you wish to change the
41 | extensions searched just assign a new `far` iterable. To disable, assign any falsy value.
42 | """
43 |
44 | base = abspath(base)
45 |
46 | @staticmethod
47 | def static_handler(context, *parts, **kw):
48 | path = normpath(pathjoin(base, *parts))
49 |
50 | if __debug__:
51 | log.debug("Attempting to serve static file.", extra=dict(
52 | request = id(context),
53 | base = base,
54 | path = path
55 | ))
56 |
57 | if not path.startswith(base): # Ensure we only serve files from the allowed path.
58 | raise HTTPForbidden("Cowardly refusing to violate base path policy." if __debug__ else None)
59 |
60 | if not exists(path): # Do the right thing if the file doesn't actually exist.
61 | raise HTTPNotFound()
62 |
63 | if not isfile(path): # Only serve normal files; no UNIX domain sockets, FIFOs, etc., etc.
64 | raise HTTPForbidden("Cowardly refusing to open a non-file." if __debug__ else None)
65 |
66 | if far and path.rpartition('.')[2] in far:
67 | context.response.cache_expires = 60*60*24*365
68 |
69 | if mapping: # Handle the mapping of filename extensions to 2-tuples. 'Cause why not?
70 | _, _, extension = basename(path).partition('.')
71 | if extension in mapping:
72 | return mapping[extension] + ':' + path, dict()
73 |
74 | return open(path, 'rb')
75 |
76 | return static_handler
77 |
--------------------------------------------------------------------------------
/docs/example/debugger.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | debugger.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | debugger.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | A one-function WebCore 2 demonstration application.
29 | Applications can be as simple or as complex and layered as your needs dictate.
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
45 | def basic ( context , name = "world" ):
46 | return "Hello {name}." . format ( name = name )
47 |
48 |
49 | if __name__ == '__main__' :
50 | from web.core import Application
51 | from web.ext.debug import DebugExtension
52 |
53 | Application ( basic , extensions = [ DebugExtension ()]) . serve ( 'waitress' )
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Generated by Dycco .
62 | Last updated 25 Apr 2016 .
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/docs/example/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | basic.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | basic.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | A one-function WebCore 2 demonstration application.
29 | Applications can be as simple or as complex and layered as your needs dictate.
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
42 | Say hello.
43 | This can be tested easily using cURL from the command line:
44 | curl http://localhost:8080/ # Default value via GET.
45 | curl http://localhost:8080/Alice # Positionally specified via GET.
46 | curl -d name=Eve http://localhost:8080/ # Form-encoded value via POST.
47 |
48 |
49 |
50 | def basic ( context , name = "world" ):
51 | return "Hello {name}." . format ( name = name )
52 |
53 |
54 | if __name__ == '__main__' :
55 | from web.core import Application
56 |
57 | Application ( basic ) . serve ( 'waitress' )
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | Generated by Dycco .
66 | Last updated 25 Apr 2016 .
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/web/ext/serialize.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """An example, though quite usable extension to handle list and dictionary return values."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | import pkg_resources
10 | from typing import Mapping
11 |
12 | from marrow.package.host import PluginManager
13 | from web.core.compat import str
14 |
15 |
16 | try:
17 | from bson import json_util as json
18 | except ImportError:
19 | import json
20 |
21 |
22 | # ## Module Globals
23 |
24 | log = __import__('logging').getLogger(__name__)
25 | json # Satisfy linter.
26 |
27 |
28 | # ## Plugin Management
29 |
30 |
31 | class SerializationPlugins(PluginManager):
32 | def __init__(self, namespace, folders=None):
33 | self.__dict__['names'] = set()
34 | self.__dict__['types'] = set()
35 | super(SerializationPlugins, self).__init__(namespace, folders)
36 |
37 | def register(self, name, plugin):
38 | super(SerializationPlugins, self).register(name, plugin)
39 |
40 | self.names.add(name)
41 |
42 | if '/' in name:
43 | self.types.add(name)
44 |
45 | def _register(self, dist):
46 | try:
47 | super(SerializationPlugins, self)._register(dist)
48 | except pkg_resources.DistributionNotFound:
49 | pass
50 |
51 |
52 | # ## Extension
53 |
54 | class SerializationExtension(object):
55 | """Sample extension demonstrating integration of automatic serialization, such as JSON.
56 |
57 | This extension registers handlers for lists and dictionaries (technically list and mappings).
58 |
59 | Additional serializers can be registered during runtime by other extensions by adding a new mimetype mapping
60 | to the `context.serialize` dictionary-like object. For convienence the default serializers are also provided
61 | using their simple names, so you can access the JSON encoder directly, for example:
62 |
63 | context.serialize.json.dumps(...)
64 | """
65 |
66 | provides = {'serialization'}
67 | extensions = {'web.serializer'}
68 | context = {'serialize'}
69 |
70 | def __init__(self, default='application/json', types=(list, Mapping)):
71 | self.default = default
72 | self.types = types
73 |
74 | # ### Application-Level Callbacks
75 |
76 | def start(self, context):
77 | if __debug__:
78 | log.debug("Registering serialization return value handlers.")
79 |
80 | manager = SerializationPlugins('web.serialize')
81 | manager.__dict__['__isabstractmethod__'] = False
82 |
83 | context.serialize = manager
84 |
85 | # Register the serialization views supported by this extension.
86 | for kind in self.types:
87 | context.view.register(kind, self.render_serialization)
88 |
89 | # ### Views
90 |
91 | def render_serialization(self, context, result):
92 | """Render serialized responses."""
93 |
94 | resp = context.response
95 | serial = context.serialize
96 | match = context.request.accept.best_match(serial.types, default_match=self.default)
97 | result = serial[match](result)
98 |
99 | if isinstance(result, str):
100 | result = result.decode('utf-8')
101 |
102 | resp.charset = 'utf-8'
103 | resp.content_type = match
104 | resp.text = result
105 |
106 | return True
107 |
108 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WebCore 2.0 Documentation
5 |
6 |
7 |
8 |
9 |
10 | WebCore 2.0 Documentation
11 |
12 | In Marrow Open Source Collective projects we aim to have our code be sufficient documentation. This collection is temporary until something more permanent can be arranged.
13 |
14 | web.core
15 |
16 |
25 |
26 | web.app
27 |
28 |
29 | web.app.static — delivery of static files from on-disk, primarily for development use
30 |
31 |
32 | web.ext
33 |
34 |
35 | web.ext.analytics — record performance statistics
36 | web.ext.annotation — utilize Python 3 function annotations for typecasting
37 | web.ext.base — construct request and response objects, and register default views
38 | web.ext.debug — an interactive web-based REPL shell and capture of unhandled excpetions for interactive debugging
39 | web.ext.local — thread local access to the context for those situations where such a solution just can't be avoided
40 |
41 |
42 | web.server
43 |
44 |
55 |
56 | Examples
57 |
58 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/docs/core/__init__.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | __init__.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | __init__.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Imports
29 |
30 |
31 | from threading import local as __local
32 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Expose these as importable from the top-level web.core namespace.
42 |
43 |
44 | from .application import Application
45 | from .util import lazy
46 |
47 |
48 |
49 |
50 |
51 |
52 |
55 | Module Globals
56 |
57 |
58 | __all__ = [ 'local' , 'Applicaiton' , 'lazy' ] # Symbols exported by this package.
59 |
60 |
61 |
62 |
63 |
64 |
65 |
68 | This is to support the web.ext.local extension, and allow for early importing of the variable.
69 |
70 |
71 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Generated by Dycco .
80 | Last updated 25 Apr 2016 .
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/web/ext/annotation.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Python 3 function annotation typecasting support."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | from inspect import ismethod, getfullargspec
10 |
11 | from web.core.compat import items
12 |
13 |
14 | # ## Extension
15 |
16 | class AnnotationExtension(object):
17 | """Utilize Python 3 function annotations as a method to filter arguments coming in from the web.
18 |
19 | Argument annotations are treated as callbacks to execute, passing in the unicode value coming in from the web and
20 | swapping it with the value returned by the callback. This allows for trivial typecasting to most built-in Python
21 | types such as `int`, `float`, etc., as well as creative use such as `','.split` to automatically split a comma-
22 | separated value. One can of course also write custom callbacks, notably ones that raise `HTTPException`
23 | subclasses to not appear as an Internal Server Error.
24 |
25 | For example:
26 |
27 | def multiply(a: int, b: int):
28 | return str(a * b)
29 |
30 | This extension also performs a utility wrapping of returned values in the form of a 2-tuple of the return
31 | annotation itself and the value returned by the callable endpoint. This integrates well with the view registered
32 | by the `web.template` package to define a template at the head of the function, returning data for the template
33 | to consume:
34 |
35 | def hello(name="world"): -> 'mako:hello.html'
36 | return dict(name=name)
37 |
38 | If your editor has difficulty syntax highlighting such annotations, check for a Python 3 compatible update to your
39 | editor's syntax definitions.
40 | """
41 |
42 | __slots__ = tuple()
43 |
44 | provides = ['annotation', 'cast', 'typecast'] # Export these symbols for other extensions to depend upon.
45 |
46 | # ### Request-Local Callbacks
47 |
48 | def mutate(self, context, handler, args, kw):
49 | """Inspect and potentially mutate the given handler's arguments.
50 |
51 | The args list and kw dictionary may be freely modified, though invalid arguments to the handler will fail.
52 | """
53 | def cast(arg, val):
54 | if arg not in annotations:
55 | return
56 |
57 | cast = annotations[key]
58 |
59 | try:
60 | val = cast(val)
61 | except (ValueError, TypeError) as e:
62 | parts = list(e.args)
63 | parts[0] = parts[0] + " processing argument '{}'".format(arg)
64 | e.args = tuple(parts)
65 | raise
66 |
67 | return val
68 |
69 | annotations = getattr(handler.__func__ if hasattr(handler, '__func__') else handler, '__annotations__', None)
70 | if not annotations:
71 | return
72 |
73 | argspec = getfullargspec(handler)
74 | arglist = list(argspec.args)
75 |
76 | if ismethod(handler):
77 | del arglist[0]
78 |
79 | for i, value in enumerate(list(args)):
80 | key = arglist[i]
81 | if key in annotations:
82 | args[i] = cast(key, value)
83 |
84 | # Convert keyword arguments
85 | for key, value in list(items(kw)):
86 | if key in annotations:
87 | kw[key] = cast(key, value)
88 |
89 | def transform(self, context, handler, result):
90 | """Transform the value returned by the controller endpoint.
91 |
92 | This extension transforms returned values if the endpoint has a return type annotation.
93 | """
94 | handler = handler.__func__ if hasattr(handler, '__func__') else handler
95 | annotation = getattr(handler, '__annotations__', {}).get('return', None)
96 |
97 | if annotation:
98 | return (annotation, result)
99 |
100 | return result
101 |
--------------------------------------------------------------------------------
/docs/example/hello.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | hello.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | hello.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | A callable class example.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 |
42 |
43 |
44 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 |
57 | def __init__ ( self , context ):
58 | self . _ctx = context
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
69 | / -- 500
70 | /?name=Bob
71 | / POST name=bob
72 | /Bob
73 |
74 |
75 | def __call__ ( self , name ):
76 | return "Hello " + name
77 |
78 |
79 | if __name__ == '__main__' :
80 | from web.core import Application
81 | Application ( Root ) . serve ( 'wsgiref' )
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | Generated by Dycco .
90 | Last updated 25 Apr 2016 .
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/web/core/util.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """WebCore common utilities."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | import logging
10 |
11 | from threading import RLock
12 |
13 | from marrow.package.canonical import name
14 |
15 |
16 | # ## Module Global Constants
17 |
18 | sentinel = object() # A singleton value to allow `None` as a legal value.
19 |
20 |
21 | # ## Utility Functions
22 |
23 | def safe_name(thing):
24 | """Attempt to resolve the canonical name for an object, falling back on the `repr()` if unable to do so."""
25 | try:
26 | return name(thing)
27 | except:
28 | return repr(thing)
29 |
30 |
31 | # ## Context-Related Utility Classes
32 |
33 | class lazy(object):
34 | """Lazily record the result of evaluating a function and cache the result.
35 |
36 | This is a non-data descriptor which tells Python to allow the instance __dict__ to override. Intended to be used
37 | by extensions to add zero-overhead (if un-accessed) values to the context.
38 | """
39 |
40 | def __init__(self, func, name=None, doc=None):
41 | self.__name__ = name or func.__name__
42 | self.__module__ = func.__module__
43 | self.__doc__ = func.__doc__
44 | self.lock = RLock()
45 | self.func = func
46 |
47 | def __get__(self, instance, type=None):
48 | if instance is None: # Allow direct access to the non-data descriptor via the class.
49 | return self
50 |
51 | with self.lock: # Try to avoid situations with parallel thread access hammering the function.
52 | value = instance.__dict__.get(self.__name__, sentinel)
53 |
54 | if value is sentinel:
55 | value = instance.__dict__[self.__name__] = self.func(instance)
56 |
57 | return value
58 |
59 |
60 | def addLoggingLevel(levelName, levelNum, methodName=None):
61 | """Comprehensively add a new logging level to the `logging` module and the current logging class.
62 |
63 | `levelName` becomes an attribute of the `logging` module with the value `levelNum`. `methodName` becomes a
64 | convenience method for both `logging` itself and the class returned by `logging.getLoggerClass()` (usually just
65 | `logging.Logger`). If `methodName` is not specified, `levelName.lower()` is used.
66 |
67 | To avoid accidental clobberings of existing attributes, this method will raise an `AttributeError` if the level
68 | name is already an attribute of the `logging` module or if the method name is already present.
69 |
70 | From: https://stackoverflow.com/a/35804945/211827
71 |
72 | Example
73 | -------
74 | >>> addLoggingLevel('TRACE', logging.DEBUG - 5)
75 | >>> logging.getLogger(__name__).setLevel("TRACE")
76 | >>> logging.getLogger(__name__).trace('that worked')
77 | >>> logging.trace('so did this')
78 | >>> logging.TRACE
79 | 5
80 | """
81 |
82 | if not methodName:
83 | methodName = levelName.lower()
84 |
85 | if hasattr(logging, levelName):
86 | raise AttributeError('{} already defined in logging module'.format(levelName))
87 | if hasattr(logging, methodName):
88 | raise AttributeError('{} already defined in logging module'.format(methodName))
89 | if hasattr(logging.getLoggerClass(), methodName):
90 | raise AttributeError('{} already defined in logger class'.format(methodName))
91 |
92 | # This method was inspired by the answers to Stack Overflow post
93 | # http://stackoverflow.com/q/2183233/2988730, especially
94 | # http://stackoverflow.com/a/13638084/2988730
95 | def logForLevel(self, message, *args, **kwargs):
96 | if self.isEnabledFor(levelNum):
97 | self._log(levelNum, message, args, **kwargs)
98 | def logToRoot(message, *args, **kwargs):
99 | logging.log(levelNum, message, *args, **kwargs)
100 |
101 | logging.addLevelName(levelNum, levelName)
102 | setattr(logging, levelName, levelNum)
103 | setattr(logging.getLoggerClass(), methodName, logForLevel)
104 | setattr(logging, methodName, logToRoot)
105 |
--------------------------------------------------------------------------------
/docs/server/appengine.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | appengine.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | appengine.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Python-standard reference servers for development use.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Imports
42 |
43 |
44 | from __future__ import unicode_literals
45 |
46 | import warnings
47 |
48 | from google.appengine.ext.webapp.util import run_wsgi_app
49 |
50 |
51 |
52 |
53 |
54 |
55 |
58 | Server Adapter
59 |
60 |
61 |
63 |
64 |
65 |
66 |
67 |
68 |
71 | Google App Engine adapter, CGI.
72 | Note: This adapter is essentially untested, and likely duplicates the cgiref adapter.
73 |
74 |
75 | def appengine ( application ):
76 |
77 | warnings . warn ( "Interactive debugging and other persistence-based processes will not work." )
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
88 | Bridge the current CGI request.
89 |
90 |
91 | run_wsgi_app ( application )
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | Generated by Dycco .
100 | Last updated 25 Apr 2016 .
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/docs/example/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | template.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | template.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Template rendering sample application.
29 | This renders the test.html file contained in the current working directory.
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
42 |
43 |
44 |
45 | def template ( context ):
46 | context . log . info ( "Returning template result." )
47 | return 'mako:./template.html' , dict ()
48 |
49 |
50 | if __name__ == '__main__' :
51 | from web.core import Application
52 | from web.ext.template import TemplateExtension
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
63 | Create the underlying WSGI application, passing the extensions to it.
64 |
65 |
66 | app = Application ( template , extensions = [ TemplateExtension ()])
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
77 | Start the development HTTP server.
78 |
79 |
80 | app . serve ( 'wsgiref' )
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | Generated by Dycco .
89 | Last updated 25 Apr 2016 .
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/docs/server/gevent_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | gevent_.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | gevent_.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Gevent-based WSGI server adapter.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Imports
42 |
43 |
44 | from __future__ import unicode_literals , print_function
45 |
46 | from gevent.pywsgi import WSGIServer
47 |
48 |
49 |
50 |
51 |
52 |
53 |
56 | Server Adapter
57 |
58 |
59 |
61 |
62 |
63 |
64 |
65 |
66 |
69 | Gevent-based WSGI-HTTP server.
70 |
71 |
72 | def serve ( application , host = '127.0.0.1' , port = 8080 ):
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
83 | Instantiate the server with a host/port configuration and our application.
84 |
85 |
86 | WSGIServer (( host , int ( port )), application ) . serve_forever ()
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | Generated by Dycco .
95 | Last updated 25 Apr 2016 .
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/example/future/monster.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Everything all in one convienent file.
4 |
5 | Non-functional until the helpers get worked on a bit more.
6 | """
7 |
8 | from web.dialect.route import route
9 | from web.dialect.helper import Resource, Collection, method, render, require
10 |
11 | from marrow.package.loader import load
12 |
13 |
14 | # REST Resources
15 |
16 | class Person(Resource):
17 | def get(self):
18 | return "Person details."
19 |
20 | def post(self):
21 | # Often not supported!
22 | return "Create a child object."
23 |
24 | def put(self):
25 | return "Replace or create this person."
26 |
27 | def delete(self):
28 | return "Delete this person."
29 |
30 |
31 | class People(Collection):
32 | __resource__ = Person
33 |
34 | def get(self):
35 | return "List of people."
36 |
37 | def post(self):
38 | return "Create new person."
39 |
40 | def put(self):
41 | return "Replace all people."
42 |
43 | def delete(self):
44 | return "Delete all people."
45 |
46 |
47 | # Efficient URL Routing
48 |
49 | class Routed(object):
50 | __dispatch__ = 'route'
51 |
52 | @route('/')
53 | def index(self):
54 | return "Root handler."
55 |
56 | @route('/page/{name}')
57 | def page(self, name):
58 | return "Page handler for: " + name
59 |
60 |
61 | # Object Dispatch (and root)
62 |
63 | class Root(object):
64 | # Attach the REST resource collection and routed "sub-directories".
65 | people = People
66 | diz = Routed
67 |
68 | # We can easily load from another module without cluttering globals.
69 | foo = load('myapp.controllers.foo:FooController')
70 |
71 | # The following is a static page definition.
72 | about = 'myapp.templates.about', dict()
73 |
74 | # This works, too! In fact, you can use any registry-handleable value!
75 | readme = open('../README.textile', 'r')
76 |
77 | def __init__(self, context):
78 | self._ctx = context
79 |
80 | def __call__(self):
81 | """Handle "index" lookups."""
82 | return "Path: /"
83 |
84 | def index(self):
85 | """Handle calls to /index -- this is no longer the 'default' index lookup."""
86 | return "Path: /index"
87 |
88 | def template(self):
89 | self._ctx.context.log.warning("Returning template result.")
90 | return 'mako:./test.html', dict()
91 |
92 | # If HTTP verbs are your thing...
93 |
94 | @method.get
95 | def login(self):
96 | return "Present login form."
97 |
98 | @login.post
99 | def login(self, **data):
100 | # can call login.get() to explicitly call that handler.
101 | return "Actually log in."
102 |
103 | # Or conditional template / serializer usage based on filename extension:
104 |
105 | @render('mako:myapp.templates.details')
106 | @render('json:') # used if request.format == 'json'
107 | def details(self):
108 | return dict(name="Bob", age=27)
109 |
110 | # Or straight-up if/elif/else:
111 |
112 | @require(predicate)
113 | def foo(self):
114 | return "We matched the predicate."
115 |
116 | @foo.require(other_predicate)
117 | def foo(self):
118 | return "We matched a different predicate."
119 |
120 | @foo.otherwise
121 | def foo(self):
122 | return "We didn't match anything. :("
123 |
124 | # If you need to be able to dynamically load the next path element...
125 |
126 | def __getattr__(self, name):
127 | if name.isdigit():
128 | return lambda: "Numerical lookup!"
129 |
130 | raise AttributeError()
131 |
132 | # Or dynamically redirect object dispatch, possibly consuming *multiple* path elements...
133 |
134 | def __lookup__(self, *parts, **data):
135 | return Controller(), ()
136 |
137 |
138 |
139 | if __name__ == '__main__':
140 | from web.core.application import Application
141 | from marrow.server.http import HTTPServer
142 |
143 | HTTPServer('127.0.0.1', 8080, application=Application(Root)).start()
144 |
--------------------------------------------------------------------------------
/docs/server/diesel_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | diesel_.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | diesel_.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Diesel-based WSGI server adapter.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Imports
42 |
43 |
44 | from __future__ import unicode_literals , print_function
45 |
46 | from diesel.protocols.wsgi import WSGIApplication
47 |
48 |
49 |
50 |
51 |
52 |
53 |
56 | Server Adapter
57 |
58 |
59 |
61 |
62 |
63 |
64 |
65 |
66 |
69 | Diesel-based (greenlet) WSGI-HTTP server.
70 | As a minor note, this is crazy. Diesel includes Flask, too.
71 |
72 |
73 | def serve ( application , host = '127.0.0.1' , port = 8080 ):
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
84 | Instantiate the server with a host/port configuration and our application.
85 |
86 |
87 | WSGIApplication ( application , port = int ( port ), iface = host ) . run ()
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Generated by Dycco .
96 | Last updated 25 Apr 2016 .
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/docs/server/eventlet_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | eventlet_.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | eventlet_.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Eventlet-based WSGI server adapter.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Imports
42 |
43 |
44 | from __future__ import unicode_literals , print_function
45 |
46 | from eventlet import listen
47 | from eventlet.wsgi import server
48 |
49 |
50 |
51 |
52 |
53 |
54 |
57 | Server Adapter
58 |
59 |
60 |
62 |
63 |
64 |
65 |
66 |
67 |
70 | Eventlet-based WSGI-HTTP server.
71 | For a more fully-featured Eventlet-capable interface, see also Spawning .
72 |
73 |
74 | def serve ( application , host = '127.0.0.1' , port = 8080 ):
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
85 | Instantiate the server with a bound port and with our application.
86 |
87 |
88 | server ( listen ( host , int ( port )), application )
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | Generated by Dycco .
97 | Last updated 25 Apr 2016 .
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/web/core/context.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """A `MutableMapping` subclass for use as a request-local context object."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | from typing import MutableMapping
10 |
11 |
12 | # ## Mapping Class
13 |
14 | class Context(MutableMapping):
15 | """An attribute access dictionary, of a kind.
16 |
17 | This utility class is used to cooperatively construct the ApplicationContext (and subsequent RequestContext)
18 | from the contributions of multiple extensions. The concept of "promotion to a class" is needed in order to enable
19 | the use of descriptor protocol attributes; without promotion the protocol would not be utilized.
20 | """
21 |
22 | # M-Morty! We're, *belch*, gonna have to go in deep, Morty! Elbow deep!
23 | def _promote(self, name, instantiate=True):
24 | """Create a new subclass of Context which incorporates instance attributes and new descriptors.
25 |
26 | This promotes an instance and its instance attributes up to being a class with class attributes, then
27 | returns an instance of that class.
28 | """
29 |
30 | metaclass = type(self.__class__)
31 | contents = self.__dict__.copy()
32 | cls = metaclass(str(name), (self.__class__, ), contents)
33 |
34 | if instantiate:
35 | return cls()
36 |
37 | return cls
38 |
39 | def __init__(self, **kw):
40 | """Construct a new Context instance.
41 |
42 | All keyword arguments are applied to the instance as attributes through direct assignment to `__dict__`.
43 | """
44 | self.__dict__.update(kw)
45 | super(Context, self).__init__()
46 |
47 | def __len__(self):
48 | """Get a list of the public data attributes."""
49 | return len([i for i in (set(dir(self)) - self._STANDARD_ATTRS) if i[0] != '_'])
50 |
51 | def __iter__(self):
52 | """Iterate all valid (public) attributes/keys."""
53 | return (i for i in (set(dir(self)) - self._STANDARD_ATTRS) if i[0] != '_')
54 |
55 | def __getitem__(self, name):
56 | """Retrieve an attribute through dictionary access."""
57 | try:
58 | return getattr(self, name)
59 | except AttributeError:
60 | pass
61 |
62 | # We do this here to avoid Python 3's nested exception support.
63 | raise KeyError(name)
64 |
65 | def __setitem__(self, name, value):
66 | """Assign an attribute through dictionary access."""
67 | setattr(self, name, value)
68 |
69 | def __delitem__(self, name):
70 | """Delete an attribute through dictionary access."""
71 | try:
72 | return delattr(self, name)
73 | except AttributeError:
74 | pass
75 |
76 | # We do this here to avoid Python 3's nested exception support.
77 | raise KeyError(name)
78 |
79 | # We generally want to exclude "default object attributes" from the context's list of attributes.
80 | # This auto-detects the basic set of them for exclusion from iteration in the above methods.
81 | Context._STANDARD_ATTRS = set(dir(Context()))
82 |
83 |
84 |
85 | class ContextGroup(Context):
86 | """A managed group of related context additions.
87 |
88 | This proxies most attribute access through to the "default" group member.
89 |
90 | Because of the possibility of conflicts, all attributes are accessible through dict-like subscripting.
91 |
92 | Register new group members through dict-like subscript assignment as attribute assignment is passed through to the
93 | default handler if assigned.
94 | """
95 |
96 | default = None
97 |
98 | def __init__(self, default=None, **kw):
99 | if default:
100 | self.__dict__['default'] = default # Avoid attribute assignment protocol for this one.
101 |
102 | for name in kw:
103 | kw[name].__name__ = name
104 | self.__dict__[name] = kw[name]
105 |
106 | def __repr__(self):
107 | return "{0.__class__.__name__}({1})".format(self, ', '.join(sorted(self)))
108 |
109 | def __len__(self):
110 | return len(self.__dict__)
111 |
112 | def __iter__(self):
113 | return iter(set(dir(self)) - self._STANDARD_ATTRS)
114 |
115 | def __getitem__(self, name):
116 | try:
117 | return getattr(self, name)
118 | except AttributeError:
119 | pass
120 |
121 | raise KeyError()
122 |
123 | def __setitem__(self, name, value):
124 | self.__dict__[name] = value
125 |
126 | def __delitem__(self, name):
127 | del self.__dict__[name]
128 |
129 | def __getattr__(self, name):
130 | if self.default is None or name.startswith('_'):
131 | raise AttributeError()
132 |
133 | return getattr(self.default, name)
134 |
135 | def __setattr__(self, name, value):
136 | if self.default is not None:
137 | return setattr(self.default, name, value)
138 |
139 | self.__dict__[name] = value
140 |
141 | def __delattr__(self, name):
142 | if self.default is not None:
143 | return delattr(self.default, name)
144 |
145 | self.__dict__[name] = None
146 | del self.__dict__[name]
147 |
148 | ContextGroup._STANDARD_ATTRS = set(dir(ContextGroup()))
149 |
150 |
--------------------------------------------------------------------------------
/docs/server/waitress_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | waitress_.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | waitress_.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | The recommended development HTTP server.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Imports
42 |
43 |
44 | from __future__ import unicode_literals , print_function
45 |
46 | try :
47 | from waitress import serve as serve_
48 | except ImportError :
49 | print ( "You must install the 'waitress' package." )
50 | raise
51 |
52 |
53 |
54 |
55 |
56 |
57 |
60 | Server Adapter
61 |
62 |
63 |
65 |
66 |
67 |
68 |
69 |
70 |
73 | The recommended development HTTP server.
74 | Note that this server performs additional buffering and will not honour chunked encoding breaks.
75 |
76 |
77 | def serve ( application , host = '127.0.0.1' , port = 8080 , threads = 4 , ** kw ):
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
88 | Bind and start the server; this is a blocking process.
89 |
90 |
91 | serve_ ( application , host = host , port = int ( port ), threads = int ( threads ), ** kw )
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | Generated by Dycco .
100 | Last updated 25 Apr 2016 .
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/test/test_core/test_dispatch.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | import pytest
4 | from inspect import isclass
5 | from unittest import TestCase
6 | from webob import Request
7 | from web.core.context import Context
8 | from web.core.dispatch import WebDispatchers
9 |
10 |
11 | class MockDispatcher(object):
12 | def __init__(self, handler, endpoint=True):
13 | self.handler = handler
14 | self.endpoint = endpoint
15 |
16 | def __call__(self, context, handler, path):
17 | if self.handler:
18 | yield path.popleft(), self.handler, self.endpoint
19 | return
20 |
21 | raise LookupError()
22 |
23 |
24 | class MockController(object):
25 | def __init__(self, context=None):
26 | self._ctx = context
27 |
28 | def endpoint(self):
29 | return "endpoint"
30 |
31 |
32 | class TransitionTargetController(object):
33 | __dispatch__ = MockDispatcher(lambda: "Hi.")
34 |
35 | def __init__(self, context=None):
36 | pass
37 |
38 |
39 | class OtherTargetController(object):
40 | __dispatch__ = MockDispatcher(TransitionTargetController())
41 |
42 | def __init__(self, context=None):
43 | pass
44 |
45 |
46 | class DispatchTransitionController(object):
47 | def __init__(self, context):
48 | pass
49 |
50 | foo = TransitionTargetController
51 | bar = OtherTargetController
52 |
53 |
54 | class DispatchBase(TestCase):
55 | def setUp(self):
56 | self._ctx = Context(
57 | request = Request.blank('/foo/bar'),
58 | extension = Context(signal=Context(dispatch=[])),
59 | dispatched = False,
60 | callback = False,
61 | events = [],
62 | )
63 | self.dispatch = self._ctx.dispatch = WebDispatchers(self._ctx)
64 | self._ctx.environ = self._ctx.request.environ
65 |
66 |
67 | class TestDispatchProtocol(DispatchBase):
68 | def wire(self, callback):
69 | self._ctx.extension = Context(signal=Context(dispatch=[callback]))
70 |
71 | def mock_dispatcher(self, context, handler, path):
72 | assert context is self._ctx
73 | context.dispatched = True
74 | yield path, handler, True
75 |
76 | def mock_no_dispatch(self, context, handler, path):
77 | assert context is self._ctx
78 | context.dispatched = True
79 | yield None, handler, False
80 |
81 | #def mock_dispatcher(self, context, handler, path):
82 | # assert context is self._ctx
83 | # context.dispatched = True
84 | # yield path, handler, True
85 |
86 | def mock_explody_dispatcher(self, context, handler, path):
87 | assert context is self._ctx
88 | context.dispatched = True
89 | raise LookupError()
90 |
91 | def basic_endpoint(self, dispatcher):
92 | def closure(context): pass
93 | closure.__dispatch__ = dispatcher
94 | return closure
95 |
96 | def callback(self, context, consumed, handler, is_endpoint):
97 | context.callback = True
98 |
99 | def test_basic_mock(self):
100 | endpoint = self.basic_endpoint(self.mock_dispatcher)
101 | is_endpoint, handler = self.dispatch(self._ctx, endpoint, self._ctx.request.path)
102 | assert is_endpoint
103 | assert handler is endpoint
104 | assert self._ctx.dispatched
105 |
106 | def test_dispatch_failure(self):
107 | endpoint = self.basic_endpoint(self.mock_explody_dispatcher)
108 | is_endpoint, handler = self.dispatch(self._ctx, endpoint, self._ctx.request.path)
109 | assert not is_endpoint
110 | assert handler is None
111 | assert self._ctx.dispatched
112 |
113 | def test_dispatch_going_nowhere(self):
114 | endpoint = self.basic_endpoint(self.mock_no_dispatch)
115 | is_endpoint, handler = self.dispatch(self._ctx, endpoint, self._ctx.request.path)
116 | assert not is_endpoint
117 | assert handler is None
118 | assert self._ctx.dispatched
119 |
120 | def test_stanard_controller(self):
121 | self._ctx.request = Request.blank('/endpoint')
122 | is_endpoint, handler = self.dispatch(self._ctx, MockController, self._ctx.request.path)
123 |
124 | assert is_endpoint
125 | assert handler() == "endpoint"
126 |
127 | def test_dispatch_transition(self):
128 | self._ctx.request = Request.blank('/foo/bar')
129 | is_endpoint, handler = self.dispatch(self._ctx, DispatchTransitionController, self._ctx.request.path)
130 |
131 | assert is_endpoint
132 | assert handler() == "Hi."
133 |
134 | self._ctx.request = Request.blank('/bar/baz/diz')
135 | is_endpoint, handler = self.dispatch(self._ctx, DispatchTransitionController, self._ctx.request.path)
136 |
137 | assert is_endpoint
138 | assert handler() == "Hi."
139 |
140 |
141 | class TestDispatchPlugins(DispatchBase):
142 | def test_existing_callable_lookup(self):
143 | def closure(): pass
144 |
145 | assert self.dispatch[closure] is closure
146 |
147 | def test_class_instantiation(self):
148 | class Closure(object): pass
149 | result = self.dispatch[Closure]
150 |
151 | assert isinstance(result, Closure)
152 |
153 | def test_named_cache(self):
154 | if 'object' not in self.dispatch.named:
155 | pytest.skip("missing web.dispatch.object")
156 |
157 | assert isclass(self.dispatch.named['object'])
158 |
159 | dispatcher = self.dispatch['object']
160 |
161 | assert 'Object' in dispatcher.__class__.__name__
162 | assert 'object' in self.dispatch.named
163 | assert self.dispatch.named['object'] is dispatcher
164 |
165 |
--------------------------------------------------------------------------------
/example/controller.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Basic class-based demonstration application.
4 |
5 | Applications can be as simple or as complex and layered as your needs dictate.
6 | """
7 |
8 | import os
9 |
10 | from web.app.static import static
11 |
12 |
13 | class Another(object): # On Python 3 you'd leave off the `(object)` bit everywhere you see it in this example.
14 | """A child controller class.
15 |
16 | This is "mounted" to the `Root` class, below, as an attribute named `child`; only the attribute name is
17 | particularly important as it is used as the matched path element during object dispatch descent.
18 |
19 | Controller classes effectively represent a template for a directory in a virtual filesystem. Assigning a class to
20 | an attribute "creates the folder" in the containing class, where callable attributes like methods are "files".
21 | Dispatch will take care of instantiating any class children, as requested during descent on each request, however,
22 | because this child controller is statically mounted, no arguments will be passed to the constructor other than the
23 | current `RequestContext`.
24 |
25 | *Important note:* Class instances are request-local. This might not equate to thread local depending on your host
26 | environment and WSGI server. Try to only rely on what is given to your controller in the context, either passing
27 | the context itself down, or by extracting the values from it that your controller would need later. Use of global
28 | state would prevent co-hosting of multiple WebCore applications in a single process, too.
29 | """
30 |
31 | def __init__(self, context):
32 | pass # We don't really care about the context, but can't just omit this method or it'll explode.
33 |
34 | def __call__(self):
35 | """Executed if the child path is requested specifically.
36 |
37 | For example:
38 |
39 | curl http://localhost:8080/child/
40 |
41 | """
42 |
43 | return "I'm the baby!"
44 |
45 | def eat(self, food="pizza"):
46 | """
47 | Executed if this endpoint is accessed.
48 |
49 | For example:
50 | curl http://localhost:8080/child/eat/
51 | curl http://localhost:8080/child/eat/sushi/
52 | """
53 |
54 | return "Yum, I love {food}!".format(food=food)
55 |
56 |
57 | class Root(object):
58 | """A basic controller class.
59 |
60 | This effectively represents the root of the virtual filesystem that is your application. Attributes from this
61 | object will be treated as child resources, as the default dispatch is object dispatch. For details, see:
62 |
63 | https://github.com/marrow/web.dispatch.object#readme
64 |
65 | Controller objects are passed the current `RequestContext` object during initialization, it's important to save
66 | this to `self` or methods won't have access to it when eventually executed. Attributes (properties and methods)
67 | whose names begin with an underscore are protected and not accessible, similar to the Linux filesystem standard
68 | of prefixing hidden files and folders with a dot. (This also convienently protects Python magic methods from
69 | direct web-based execution.)
70 |
71 | In order to extend Root into other objects such as Another, you need to register them in class scope with {path element} = {class name}.
72 | This will make the Another class available to the user as /child via Object dispatching.
73 | """
74 |
75 | __slots__ = ('_ctx', ) # This is an optimization to tell CPython that our only instance attribute is `_ctx`.
76 |
77 | child = Another
78 |
79 | def __init__(self, context):
80 | """Initialize our controller, either saving the context or getting anything we need from it."""
81 | self._ctx = context
82 |
83 | def __call__(self):
84 | """Handle "index" lookups.
85 |
86 | This is analogous to an `index.html` file, accessible as the default representation of a folder, that is
87 | otherwise not accessible as `index.html`. The presence of a `__call__` method makes the controller instance
88 | itself a valid callable endpoint.
89 | """
90 |
91 | return "Path: /"
92 |
93 | def index(self):
94 | """Handle calls to /index.
95 |
96 | WebCore 1 treated "index" lookups and the `index` method specially. WebCore 2 does not, freeing the name to be
97 | used for user-defined endpoints.
98 | """
99 | return "Path: /index"
100 |
101 | # Attributes may be assigned statically; these are not callable endpoints but rather static endpoints.
102 | # WebCore will treat access to these (via /foo, /bar, etc.) as if a callable endpoint was called and the static
103 | # value returned. This can serve any type of object there is a view registered for.
104 | string = "Static string!"
105 | source = open(__file__, 'rb')
106 |
107 | # This is a reusable endpoint (like an endpoint factory) serving static files from disk. Serving static files this
108 | # way is only really useful in development; in production serve them from a production quality front-end server,
109 | # load balancer, or CDN, such as via Nginx.
110 | example = static(os.path.dirname(__file__)) # Serve the directory this source file is in. (Don't do this. ;)
111 |
112 |
113 | if __name__ == '__main__':
114 | from web.core.application import Application
115 |
116 | Application(Root, logging={'level': 'info'}).serve('waitress', threads=15)
117 |
118 |
--------------------------------------------------------------------------------
/docs/server/fcgi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | fcgi.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | fcgi.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | A production quality flup-based FastCGI server.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Imports
42 |
43 |
44 | from __future__ import unicode_literals , print_function
45 |
46 | try :
47 | from flup.server.fcgi import WSGIServer
48 | except ImportError :
49 | print ( "You must install a 'flup' package such as 'flup6' to use FastCGI support." )
50 | raise
51 |
52 |
53 |
54 |
55 |
56 |
57 |
60 | Server Adapter
61 |
62 |
63 |
65 |
66 |
67 |
68 |
69 |
70 |
73 | Basic FastCGI support via flup.
74 | This web server has many, many options. Please see the Flup project documentation for details.
75 |
76 |
77 | def serve ( application , host = '127.0.0.1' , port = 8080 , socket = None , ** options ):
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
88 | Allow either on-disk socket (recommended) or TCP/IP socket use.
89 |
90 |
91 | if not socket :
92 | bindAddress = ( host , int ( port ))
93 | else :
94 | bindAddress = socket
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
105 | Bind and start the blocking web server interface.
106 |
107 |
108 | WSGIServer ( application , bindAddress = bindAddress , ** options ) . run ()
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Generated by Dycco .
117 | Last updated 25 Apr 2016 .
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/web/core/dispatch.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | # ## Imports
4 |
5 | from __future__ import unicode_literals
6 |
7 | from collections import deque
8 | from inspect import isclass
9 | from marrow.package.host import PluginManager
10 |
11 |
12 | # ## Module Globals
13 |
14 | # A standard logger object.
15 | log = __import__('logging').getLogger(__name__)
16 |
17 |
18 | # ## Dispatch Plugin Manager
19 |
20 | class WebDispatchers(PluginManager):
21 | """WebCore dispatch protocol adapter.
22 |
23 | The process of resolving a request path to an endpoint. The dispatch protocol utilized is documented in full
24 | in the `protocol` project: https://github.com/marrow/protocols/blob/master/dispatch/README.md
25 |
26 | The allowable controller structures offered by individual methods of dispatch is documented in the relevant
27 | dispatch project. Examples of dispatch include:
28 |
29 | * Object Dispatch: https://github.com/marrow/web.dispatch.object
30 | * Registered Routes: https://github.com/marrow/web.dispatch.route
31 | * Traversal: https://github.com/marrow/web.dispatch.traversal
32 |
33 | Others may exist, and dispatch middleware may be available to perform more complex behaviours. The default
34 | dispatcher if not otherwise configured is object dispatch.
35 | """
36 |
37 | __isabstractmethod__ = False # Work around an issue in modern (3.4+) Python due to our instances being callable.
38 |
39 | def __init__(self, ctx):
40 | """Dispatch registry constructor.
41 |
42 | The dispatch registry is not meant to be instantiated by third-party software. Instead, access the registry as
43 | an attribute of the current Application or Request context: `context.dispatch`
44 | """
45 |
46 | super(WebDispatchers, self).__init__('web.dispatch')
47 |
48 | def __call__(self, context, handler, path):
49 | """Having been bound to an appropriate context, find a handler for the request path.
50 |
51 | This is the part of the WebCore request cycle that speaks the Dispatch protocol and performs event bridging.
52 |
53 | This requires a context prepared with a `context.extension.signal.dispatch` list and dispatch plugin registry
54 | as `context.dispatch`. This does not use `self` to allow for more creative overriding.
55 | """
56 |
57 | is_endpoint = False # We'll search until we find an endpoint.
58 |
59 | # This technically doesn't help Pypy at all, but saves repeated deep lookup in CPython.(E)
60 | callbacks = context.extension.signal.dispatch # These extensions want to be notified.
61 | self = context.dispatch # Dispatch plugin registry.
62 |
63 | if __debug__:
64 | log.debug("Preparing dispatch.", extra=dict(
65 | request = id(context.request),
66 | path = context.request.path_info,
67 | handler = repr(handler)
68 | ))
69 |
70 | # Now we need the remaining path elements as a deque.
71 | path = path.strip('/')
72 | path = deque(path.split('/')) if path else deque()
73 |
74 | try:
75 | while not is_endpoint:
76 | # Pull the dispatcher out of the current handler, defaulting to object dispatch.
77 | dispatcher = self[getattr(handler, '__dispatch__', 'object')]
78 |
79 | # We don't stop if the same dispatcher is loaded twice since some dispatchers might want to do that.
80 | starting = handler
81 |
82 | # Iterate dispatch events, issuing appropriate callbacks as we descend.
83 | for crumb in dispatcher(context, handler, path):
84 | is_endpoint, handler = crumb.endpoint, crumb.handler
85 |
86 | if is_endpoint and not callable(handler) and hasattr(handler, '__dispatch__'):
87 | crumb = crumb.replace(endpoint=False)
88 |
89 | #__import__('wdb').set_trace()
90 | # DO NOT add production logging statements (ones not wrapped in `if __debug__`) to this callback!
91 | for ext in callbacks: ext(context, str(crumb.path) if crumb.path else None, crumb.handler, crumb.endpoint)
92 |
93 | # Repeat of earlier, we do this after extensions in case anything above modifies the environ path.
94 | path = context.environ['PATH_INFO'].strip('/')
95 | path = deque(path.split('/')) if path else deque()
96 |
97 | if not is_endpoint and starting is handler: # We didn't go anywhere.
98 | break
99 |
100 | # Dispatch failed utterly.
101 | except LookupError:
102 | pass # `is_endpoint` can only be `False` here.
103 |
104 | return is_endpoint, handler if is_endpoint else None
105 |
106 | def __getitem__(self, dispatcher):
107 | """Retrieve a dispatcher from the registry.
108 |
109 | This performs some additional work beyond the standard plugin manager in order to construct configured
110 | instances of the dispatchers instead of simply returning them bare. This allows for configuration and caching
111 | of these configured dispatchers to happen in a single place.
112 | """
113 |
114 | name = None
115 |
116 | if callable(dispatcher) and not isclass(dispatcher):
117 | return dispatcher
118 |
119 | # If the dispatcher isn't already executable, it's probably an entry point reference. Load it from cache.
120 | if not isclass(dispatcher):
121 | name = dispatcher
122 | dispatcher = super(WebDispatchers, self).__getitem__(dispatcher)
123 |
124 | # If it's uninstantiated, instantiate it.
125 | if isclass(dispatcher):
126 | # TODO: Extract **kw settings from context.
127 | dispatcher = dispatcher() # Instantiate the dispatcher.
128 | if name: # Update the entry point cache if loaded by name.
129 | self.named[name] = dispatcher
130 |
131 | if __debug__:
132 | log.debug("Loaded dispatcher.", extra=dict(dispatcher=repr(dispatcher)))
133 |
134 | return dispatcher
135 |
136 |
--------------------------------------------------------------------------------
/docs/server/cherrypy_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | cherrypy_.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | cherrypy_.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | CherryPy-based WSGI server adapter.
29 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
41 | Imports
42 |
43 |
44 | from __future__ import unicode_literals , print_function
45 |
46 | from cherrypy.wsgiserver import CherryPyWSGIServer
47 |
48 |
49 |
50 |
51 |
52 |
53 |
56 | Server Adapter
57 |
58 |
59 |
61 |
62 |
63 |
64 |
65 |
66 |
69 | CherryPy-based WSGI-HTTP server.
70 |
71 |
72 | def serve ( application , host = '127.0.0.1' , port = 8080 ):
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
83 | Instantiate the server with our configuration and application.
84 |
85 |
86 | server = CherryPyWSGIServer (( host , int ( port )), application , server_name = host )
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
97 | Try to be handy as many terminals allow clicking links.
98 |
99 |
100 | print ( "serving on http://{0}:{1}" . format ( host , port ))
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
111 | Bind and launch the server; this is a blocking operation.
112 |
113 |
114 | try :
115 | server . start ()
116 | except KeyboardInterrupt :
117 | server . stop () # CherryPy has some of its own shutdown work to do.
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | Generated by Dycco .
126 | Last updated 25 Apr 2016 .
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/docs/server/tornado_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tornado_.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | tornado_.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Imports
29 |
30 |
31 | from __future__ import unicode_literals , print_function
32 |
33 | try :
34 | import tornado.ioloop
35 | import tornado.httpserver
36 | import tornado.wsgi
37 | except ImportError :
38 | print ( "You must install the 'tornado' package." )
39 | raise
40 |
41 |
42 |
43 |
44 |
45 |
46 |
49 | Server Adapter
50 |
51 |
52 |
54 |
55 |
56 |
57 |
58 |
59 |
62 | Tornado's HTTPServer.
63 | This is a high quality asynchronous server with many options. For details, please visit:
64 | http://www.tornadoweb.org/en/stable/httpserver.html#http-server
65 |
66 |
67 |
68 | def serve ( application , host = '127.0.0.1' , port = 8080 , ** options ):
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
79 | Wrap our our WSGI application (potentially stack) in a Tornado adapter.
80 |
81 |
82 | container = tornado . wsgi . WSGIContainer ( application )
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
93 | Spin up a Tornado HTTP server using this container.
94 |
95 |
96 | http_server = tornado . httpserver . HTTPServer ( container , ** options )
97 | http_server . listen ( int ( port ), host )
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
108 | Start and block on the Tornado IO loop.
109 |
110 |
111 | tornado . ioloop . IOLoop . instance () . start ()
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | Generated by Dycco .
120 | Last updated 25 Apr 2016 .
121 |
122 |
123 |
124 |
125 |
--------------------------------------------------------------------------------
/web/core/extension.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """WebCore extension management.
4 |
5 | This extension registry handles loading and access to extensions as well as the collection of standard WebCore
6 | Extension API callbacks. Reference the `SIGNALS` constant for a list of the individual callbacks that can be
7 | utilized and their meanings, and the `extension.py` example for more detailed descriptions.
8 |
9 | At a basic level an extension is a class. That's it; attributes and methods are used to inform the manager
10 | of extension metadata and register callbacks for certain events. The most basic extension is one that does
11 | nothing:
12 |
13 | class Extension: pass
14 |
15 | To register your extension, add a reference to it to your project's `entry_points` in your project's `setup.py`
16 | under the `web.extension` namespace:
17 |
18 | setup(
19 | ...,
20 | entry_points = {'web.extension': [
21 | 'example = myapp:Extension',
22 | ]},
23 | )
24 |
25 | Your extension may define several additional properties:
26 |
27 | * `provides` -- declare a set of tags describing the features offered by the plugin
28 | * `needs` -- delcare a set of tags that must be present for this extension to function
29 | * `uses` -- declare a set of tags that must be evaluated prior to this extension, but aren't hard requirements
30 | * `first` -- declare that this extension is a dependency of all other non-first extensions if truthy
31 | * `last` -- declare that this extension depends on all other non-last extensions if truthy
32 | * `signals` -- a set of additional signal names declared used (thus cacheable) by the extension manager
33 |
34 | Tags used as `provides` values should also be registered as `web.extension` entry points. Additional `signals` may be
35 | prefixed with a minus symbol (-) to request reverse ordering, simulating the exit path of WSGI middleware.
36 |
37 | """
38 |
39 | # ## Imports
40 |
41 | from __future__ import unicode_literals
42 |
43 | from marrow.package.host import ExtensionManager
44 |
45 | from .compat import items
46 | from .context import Context
47 |
48 |
49 | # ## Module Globals
50 |
51 | # A standard Python logger object.
52 | log = __import__('logging').getLogger(__name__)
53 |
54 |
55 | # ## Extension Manager
56 |
57 | class WebExtensions(ExtensionManager):
58 | """Principal WebCore extension manager."""
59 |
60 | # Each of these is an optional extension callback attribute.
61 | SIGNALS = { # Core extension hooks.
62 | 'start', # Executed during Application construction.
63 | 'stop', # Executed when (and if) the serve() server returns.
64 | 'graceful', # Executed when (and if) the process is instructed to reload configuration.
65 | 'prepare', # Executed during initial request processing.
66 | 'dispatch', # Executed once for each dispatch event.
67 | 'before', # Executed after all extension `prepare` methods have been called, prior to dispatch.
68 | 'mutate', # Inspect and potentially mutate arguments to the handler prior to execution.
69 | '-after', # Executed after dispatch has returned and the response populated.
70 | '-transform', # Transform the result returned by the handler and apply it to the response.
71 | '-done', # Executed after the response has been consumed by the client.
72 | '-middleware', # Executed to allow WSGI middleware wrapping.
73 | }
74 |
75 | __isabstractmethod__ = False # Work around a Python 3.4+ issue when attaching to the context.
76 |
77 | # ### \_\_init__(ctx: _ApplicationContext_)
78 | def __init__(self, ctx):
79 | """Extension registry constructor.
80 |
81 | The extension registry is not meant to be instantiated by third-party software. Instead, access the registry
82 | as an attribute of the current Application or Request context: `context.extension`
83 |
84 | Currently, this uses some application-internal shenanigans to construct the initial extension set.
85 | """
86 |
87 | self.feature = set() # Track the active `provides` tags.
88 | all = self.all = self.order(ctx.app.config['extensions']) # Dependency ordered, active extensions.
89 |
90 | signals = {}
91 | inverse = set()
92 |
93 | # Prepare the known callback sets.
94 |
95 | def add_signal(name):
96 | if name[0] == '-':
97 | name = name[1:]
98 | inverse.add(name)
99 |
100 | signals[name] = []
101 |
102 | # Populate the initial set of signals from our own.
103 | for signal in self.SIGNALS: add_signal(signal)
104 |
105 | # Populate additional signals and general metadata provided by registered extensions.
106 | for ext in all:
107 | self.feature.update(getattr(ext, 'provides', [])) # Enable those flags.
108 | for signal in getattr(ext, 'signals', []): add_signal(signal) # And those callbacks.
109 |
110 | # Prepare the callback cache.
111 |
112 | for ext in all:
113 | for signal in signals: # Attach any callbacks that might exist.
114 | handler = getattr(ext, signal, None)
115 | if handler: signals[signal].append(handler)
116 |
117 | if hasattr(ext, '__call__'): # This one is aliased; the extension itself is treated as WSGI middleware.
118 | signals['middleware'].append(ext)
119 |
120 | # Certain operations act as a stack, i.e. "before" are executed in dependency order, but "after" are executed
121 | # in reverse dependency order. This is also the case with "mutate" (incoming) and "transform" (outgoing).
122 | for signal in inverse:
123 | signals[signal].reverse()
124 |
125 | # Transform the signal lists into tuples to compact them.
126 | self.signal = Context(**{k: tuple(v) for k, v in items(signals)})
127 |
128 | # This will save a chain() call on each request by pre-prepending the two lists.
129 | # Attempts to add extensions during runtime are complicated by this optimization.
130 | self.signal['pre'] = tuple(signals['prepare'] + signals['before'])
131 |
132 | # Continue up the chain with the `ExtensionManager` initializer, using the `web.extension` namespace.
133 | super(WebExtensions, self).__init__('web.extension')
134 |
135 |
--------------------------------------------------------------------------------
/docs/core/compat.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | compat.py
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | compat.py
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 | Compatibility helpers to bridge the differences between Python 2 and Python 3.
29 | Similar in purpose to six . Not generally intended to be used by
30 | third-party software, these are subject to change at any time. Only symbols exported via __all__ are safe to use.
31 |
32 |
33 |
35 |
36 |
37 |
38 |
39 |
40 |
43 | Imports
44 |
45 |
46 |
48 |
49 |
50 |
51 |
52 |
53 |
56 | Module Exports
57 |
58 |
59 | __all__ = [ 'py3' , 'pypy' , 'unicode' , 'str' ]
60 |
61 |
62 |
63 |
64 |
65 |
66 |
69 | Version Detection
70 |
71 |
72 | py3 = sys . version_info > ( 3 , )
73 | pypy = hasattr ( sys , 'pypy_version_info' )
74 |
75 |
76 |
77 |
78 |
79 |
80 |
83 | Builtins Compatibility
84 |
85 |
86 |
88 |
89 |
90 |
91 |
92 |
93 |
96 | Use of the items shortcut here must be very, very careful to only apply it to actual bare dictionaries.
97 |
98 |
99 | if py3 :
100 | unicode = str
101 | str = bytes
102 | items = dict . items
103 | else :
104 | unicode = unicode
105 | str = str
106 | items = dict . iteritems
107 |
108 |
109 |
110 |
111 |
112 |
113 |
116 | File-Like String Handling
117 |
118 |
119 | try :
120 | try :
121 | from cStringIO import StringIO
122 | except ImportError :
123 | from StringIO import StringIO
124 | except ImportError :
125 | from io import StringIO
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | Generated by Dycco .
134 | Last updated 25 Apr 2016 .
135 |
136 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/web/core/view.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """The WebCore view registry.
4 |
5 | WebCore uses a registry of callables to transform values returned by controllers for use as a response. This
6 | translation process is used to support the built-in types (see the `base` extension) and can be extended by your
7 | own application to support additional types. This is effectively the "view" component of Model-View-Controller.
8 |
9 | A view handler takes the result of calling a controller endpoint and applies the result to the response. It can be
10 | as simple as a bare function that accepts the current `RequestContext` instance plus the value returned by the
11 | endpoint, and either returns a truthy value to indicate successful handling, or a falsy value to indicate that this
12 | view handler could not handle the result. The simplest case is a handler that always "passes the buck" and handles
13 | nothing:
14 |
15 | def ignore(context, result):
16 | pass
17 |
18 | Slightly more useful would be to assign the result directly to the response:
19 |
20 | def stringy(context, result):
21 | context.response.text = result
22 | return True
23 |
24 | As an example pulled from the "template" extension, you can use an "exit early" strategy to selectively handle
25 | subsets of the type your view is registered against, such as only handling 2-tuples with a specific first value:
26 |
27 | def json(context, result):
28 | from json import dumps
29 | if len(result) != 2 or result[0] != 'json':
30 | return
31 | context.response.text = dumps(result)
32 | return True
33 |
34 | A view may go so far as to wholly replace the `context.response` object; any callable WSGI application can be
35 | utilized as such. Cooperative response construction is _strongly_ preferred, however.
36 |
37 | If you frequently hand back serialized data, you may be able to simplify your controller code and reduce
38 | boilerplate by simply returning your model instances. By registering a view handler for your model class you can
39 | implement the serialization in a single, centralized location, making security and testing much easier.
40 |
41 | When a controller raises an `HTTPError` subclass it is treated as the return value. As such you can take specific
42 | action on any given error by registering a view handler for the specific exception subclass (i.e. `HTTPNotFound`),
43 | such as rendering a pretty error page. By default these exceptions are treated as a WSGI application and are
44 | directly used as the response, but only if no more specific handlers are registered.
45 | """
46 |
47 | # ## Imports
48 |
49 | from __future__ import unicode_literals
50 |
51 | from webob.multidict import MultiDict
52 | from marrow.package.canonical import name
53 | from marrow.package.host import PluginManager
54 |
55 | from .compat import py3, pypy
56 |
57 |
58 | # ## Module Globals
59 |
60 | # A standard logger object.
61 | log = __import__('logging').getLogger(__name__)
62 |
63 |
64 | # ## View Registry
65 |
66 | class WebViews(PluginManager):
67 | """A `PluginManager` subclass to manage and search plugin and manually-registered views.
68 |
69 | This extends plugin naming to support multiple candidates per name, overrides manual registration to log some
70 | useful messages, and makes instances callable. Executing a `WebViews` instance passing in an object will
71 | produce a generator yielding candidate views registered to handle that type of object.
72 | """
73 |
74 | __isabstractmethod__ = False # Work around a Python 3.4+ issue, since our instances are callable.
75 |
76 | # ### Python Protocols
77 |
78 | def __init__(self, ctx):
79 | """View registry constructor.
80 |
81 | The view registry is not meant to be instantiated by third-party software. Instead, access the registry as
82 | an attribute of the the current `ApplicationContext` or `RequestContext`: `context.view`
83 | """
84 | super(WebViews, self).__init__('web.view')
85 | self.__dict__['_map'] = MultiDict()
86 |
87 | def __repr__(self):
88 | """Programmers' representation for development-time diagnostics."""
89 | return "WebViews({})".format(len(self._map))
90 |
91 | def __call__(self, result):
92 | """Identify view to use based on the type of result when our instance is called as a function.
93 |
94 | This generates a stream of candidates which should be called in turn until one returns a truthy value.
95 | """
96 |
97 | rtype = type(result)
98 |
99 | # Short-circuit on exact matches.
100 | for candidate in self._map.getall(rtype):
101 | yield candidate
102 |
103 | # More exhaustive search for potentially more crafty use such as ABC, zope.interface, marrow.interface, etc.
104 | for kind, candidate in self._map.iteritems():
105 | if kind is rtype: continue # We've already seen these.
106 |
107 | if isinstance(result, kind):
108 | yield candidate
109 |
110 | # ### Plugin Registration
111 |
112 | def register(self, kind, handler):
113 | """Register a handler for a given type, class, interface, or abstract base class.
114 |
115 | View registration should happen within the `start` callback of an extension. For example, to register the
116 | previous `json` view example:
117 |
118 | class JSONExtension:
119 | def start(self, context):
120 | context.view.register(tuple, json)
121 |
122 | The approach of explicitly referencing a view handler isn't very easy to override without also replacing the
123 | extension originally adding it, however there is another approach. Using named handlers registered as discrete
124 | plugins (via the `entry_point` argument in `setup.py`) allows the extension to easily ask "what's my handler?"
125 |
126 | class JSONExtension:
127 | def start(self, context):
128 | context.view.register(
129 | tuple,
130 | context.view.json
131 | )
132 |
133 | Otherwise unknown attributes of the view registry will attempt to look up a handler plugin by that name.
134 | """
135 | if __debug__: # In production this logging is completely skipped, regardless of logging level.
136 | if py3 and not pypy: # Where possible, we shorten things to just the cannonical name.
137 | log.debug("Registering view handler.", extra=dict(type=name(kind), handler=name(handler)))
138 | else: # Canonical name lookup is not entirely reliable on some combinations.
139 | log.debug("Registering view handler.", extra=dict(type=repr(kind), handler=repr(handler)))
140 |
141 | # Add the handler to the pool of candidates. This adds to a list instead of replacing the "dictionary item".
142 | self._map.add(kind, handler)
143 |
144 | return handler
145 |
146 |
--------------------------------------------------------------------------------
/web/ext/base.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """The base extension providing request, response, and core views."""
4 |
5 | # ## Imports
6 |
7 | from __future__ import unicode_literals
8 |
9 | from io import IOBase
10 | try:
11 | IOBase = (IOBase, file)
12 | except:
13 | pass
14 |
15 | try:
16 | from collections import Generator
17 | except ImportError:
18 | def _tmp(): yield None # pragma: no cover
19 | Generator = type(_tmp())
20 |
21 | from os.path import getmtime
22 | from time import mktime, gmtime
23 | from datetime import datetime
24 | from mimetypes import init, add_type, guess_type
25 | from collections import namedtuple
26 | from webob import Request, Response
27 |
28 | from web.core.compat import str, unicode, Path
29 | from web.core.util import safe_name
30 |
31 |
32 | # ## Module Globals
33 |
34 | log = __import__('logging').getLogger(__name__)
35 |
36 |
37 | # ## Helper Classes
38 |
39 | Crumb = namedtuple('Breadcrumb', ('handler', 'path'))
40 |
41 |
42 | class Bread(list):
43 | @property
44 | def current(self):
45 | return self[-1].path
46 |
47 |
48 |
49 | # ## Extension
50 |
51 | class BaseExtension(object):
52 | """Base framework extension.
53 |
54 | This extension is not meant to be manually constructed or manipulated; use is automatic.
55 | """
56 |
57 | first = True # Must occur as early as possible in callback lists.
58 | always = True # Always enabled.
59 | provides = ["base", "request", "response"] # Export these symbols for use as other extension's dependencies.
60 |
61 | # ### Application-Level Callbacks
62 |
63 | def start(self, context):
64 | if __debug__:
65 | log.debug("Registering core return value handlers.")
66 |
67 | # This prepares the mimetypes registry, and adds values typically missing from it.
68 | init()
69 | add_type('text/x-yaml', 'yml')
70 | add_type('text/x-yaml', 'yaml')
71 |
72 | # Register the core views supported by the base framework.
73 | register = context.view.register
74 | register(type(None), self.render_none)
75 | register(Response, self.render_response)
76 | register(str, self.render_binary)
77 | register(unicode, self.render_text)
78 | register(IOBase, self.render_file)
79 | register(Generator, self.render_generator)
80 |
81 | # ### Request-Level Callbacks
82 |
83 | def prepare(self, context):
84 | """Add the usual suspects to the context.
85 |
86 | This adds `request`, `response`, and `path` to the `RequestContext` instance.
87 | """
88 |
89 | if __debug__:
90 | log.debug("Preparing request context.", extra=dict(request=id(context)))
91 |
92 | # Bridge in WebOb `Request` and `Response` objects.
93 | # Extensions shouldn't rely on these, using `environ` where possible instead.
94 | context.request = Request(context.environ)
95 | context.response = Response(request=context.request)
96 |
97 | # Record the initial path representing the point where a front-end web server bridged to us.
98 | context.environ['web.base'] = context.request.script_name
99 |
100 | # Track the remaining (unprocessed) path elements.
101 | context.request.remainder = context.request.path_info.split('/')
102 | if context.request.remainder and not context.request.remainder[0]:
103 | del context.request.remainder[0]
104 |
105 | # Track the "breadcrumb list" of dispatch through distinct controllers.
106 | context.path = Bread()
107 |
108 | def dispatch(self, context, consumed, handler, is_endpoint):
109 | """Called as dispatch descends into a tier.
110 |
111 | The base extension uses this to maintain the "current url".
112 | """
113 |
114 | request = context.request
115 |
116 | if __debug__:
117 | log.debug("Handling dispatch event.", extra=dict(
118 | request = id(context),
119 | consumed = consumed,
120 | handler = safe_name(handler),
121 | endpoint = is_endpoint
122 | ))
123 |
124 | # The leading path element (leading slash) requires special treatment.
125 | if not consumed and context.request.path_info_peek() == '':
126 | consumed = ['']
127 |
128 | nConsumed = 0
129 | if consumed:
130 | # Migrate path elements consumed from the `PATH_INFO` to `SCRIPT_NAME` WSGI environment variables.
131 | if not isinstance(consumed, (list, tuple)):
132 | consumed = consumed.split('/')
133 |
134 | for element in consumed:
135 | if element == context.request.path_info_peek():
136 | context.request.path_info_pop()
137 | nConsumed += 1
138 | else:
139 | break
140 |
141 | # Update the breadcrumb list.
142 | context.path.append(Crumb(handler, Path(request.script_name)))
143 |
144 | if consumed: # Lastly, update the remaining path element list.
145 | request.remainder = request.remainder[nConsumed:]
146 |
147 | # ### Views
148 |
149 | def render_none(self, context, result):
150 | """Render empty responses."""
151 | context.response.body = b''
152 | del context.response.content_length
153 | return True
154 |
155 | def render_response(self, context, result):
156 | """Allow direct returning of WebOb `Response` instances."""
157 | context.response = result
158 | return True
159 |
160 | def render_binary(self, context, result):
161 | """Return binary responses unmodified."""
162 | context.response.app_iter = iter((result, )) # This wraps the binary string in a WSGI body iterable.
163 | return True
164 |
165 | def render_text(self, context, result):
166 | """Return textual responses, encoding as needed."""
167 | context.response.text = result
168 | return True
169 |
170 | def render_file(self, context, result):
171 | """Perform appropriate metadata wrangling for returned open file handles."""
172 | if __debug__:
173 | log.debug("Processing file-like object.", extra=dict(request=id(context), result=repr(result)))
174 |
175 | response = context.response
176 | response.conditional_response = True
177 |
178 | modified = mktime(gmtime(getmtime(result.name)))
179 |
180 | response.last_modified = datetime.fromtimestamp(modified)
181 | ct, ce = guess_type(result.name)
182 | if not ct: ct = 'application/octet-stream'
183 | response.content_type, response.content_encoding = ct, ce
184 | response.etag = unicode(modified)
185 |
186 | result.seek(0, 2) # Seek to the end of the file.
187 | response.content_length = result.tell()
188 |
189 | result.seek(0) # Seek back to the start of the file.
190 | response.body_file = result
191 |
192 | return True
193 |
194 | def render_generator(self, context, result):
195 | """Attempt to serve generator responses through stream encoding.
196 |
197 | This allows for direct use of cinje template functions, which are generators, as returned views.
198 | """
199 | context.response.encoding = 'utf8'
200 | context.response.app_iter = (
201 | (i.encode('utf8') if isinstance(i, unicode) else i) # Stream encode unicode chunks.
202 | for i in result if i is not None # Skip None values.
203 | )
204 | return True
205 |
206 |
--------------------------------------------------------------------------------
/web/ext/args.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 |
3 | """Argument handling extensions for WebCore applications.
4 |
5 | These allow you to customize the behaviour of the arguments passed to endpoints.
6 | """
7 |
8 | from inspect import isroutine, ismethod, getcallargs
9 |
10 | from webob.exc import HTTPNotFound
11 | from web.core.util import safe_name
12 |
13 |
14 | # A standard Python logger object.
15 | log = __import__('logging').getLogger(__name__)
16 |
17 |
18 | class ArgumentExtension(object):
19 | """Not for direct use."""
20 |
21 | @staticmethod
22 | def _process_flat_kwargs(source, kwargs):
23 | """Apply a flat namespace transformation to recreate (in some respects) a rich structure.
24 |
25 | This applies several transformations, which may be nested:
26 |
27 | `foo` (singular): define a simple value named `foo`
28 | `foo` (repeated): define a simple value for placement in an array named `foo`
29 | `foo[]`: define a simple value for placement in an array, even if there is only one
30 | `foo.`: define a simple value to place in the `foo` array at the identified index
31 |
32 | By nesting, you may define deeper, more complex structures:
33 |
34 | `foo.bar`: define a value for the named element `bar` of the `foo` dictionary
35 | `foo..bar`: define a `bar` dictionary element on the array element marked by that ID
36 |
37 | References to `` represent numeric "attributes", which makes the parent reference be treated as an array,
38 | not a dictionary. Exact indexes might not be able to be preserved if there are voids; Python lists are not
39 | sparse.
40 |
41 | No validation of values is performed.
42 | """
43 |
44 | ordered_arrays = []
45 |
46 | # Process arguments one at a time and apply them to the kwargs passed in.
47 |
48 | for name, value in source.items():
49 | container = kwargs
50 |
51 | if '.' in name:
52 | parts = name.split('.')
53 | name = name.rpartition('.')[2]
54 |
55 | for target, following in zip(parts[:-1], parts[1:]):
56 | if following.isnumeric(): # Prepare any use of numeric IDs.
57 | container.setdefault(target, [{}])
58 | if container[target] not in ordered_arrays:
59 | ordered_arrays.append(container[target])
60 | container = container[target][0]
61 | continue
62 |
63 | container = container.setdefault(target, {})
64 |
65 | if name.endswith('[]'): # `foo[]` or `foo.bar[]` etc.
66 | name = name[:-2]
67 | container.setdefault(name, [])
68 | container[name].append(value)
69 | continue
70 |
71 | if name.isnumeric() and container is not kwargs: # trailing identifiers, `foo.`
72 | container[int(name)] = value
73 | continue
74 |
75 | if name in container:
76 | if not isinstance(container[name], list):
77 | container[name] = [container[name]]
78 |
79 | container[name].append(value)
80 | continue
81 |
82 | container[name] = value
83 |
84 | for container in ordered_arrays:
85 | elements = container[0]
86 | del container[:]
87 | container.extend(value for name, value in sorted(elements.items()))
88 |
89 | @staticmethod
90 | def _process_rich_kwargs(source, kwargs):
91 | """Apply a nested structure to the current kwargs."""
92 | kwargs.update(source)
93 |
94 |
95 | class ValidateArgumentsExtension(object):
96 | """Use this to enable validation of endpoint arguments.
97 |
98 | You can determine when validation is executed (never, always, or development) and what action is taken when a
99 | conflict occurs.
100 | """
101 |
102 | last = True
103 |
104 | provides = {'args.validation', 'kwargs.validation'}
105 |
106 | def __init__(self, enabled='development', correct=False):
107 | """Configure when validation is performed and the action performed.
108 |
109 | If `enabled` is `True` validation will always be performed, if `False`, never. If set to `development` the
110 | callback will not be assigned and no code will be executed during runtime.
111 |
112 | When `correct` is falsy (the default), an `HTTPNotFound` will be raised if a conflict occurs. If truthy the
113 | conflicting arguments are removed, with positional taking precedence to keyword.
114 | """
115 |
116 | if enabled is True or (enabled == 'development' and __debug__):
117 | self.mutate = self._mutate
118 |
119 | def _mutate(self, context, endpoint, args, kw):
120 | try:
121 | if callable(endpoint) and not isroutine(endpoint):
122 | endpoint = endpoint.__call__ # Handle instances that are callable.
123 |
124 | getcallargs(endpoint, *args, **kw)
125 |
126 | except TypeError as e:
127 | # If the argument specification doesn't match, the handler can't process this request.
128 | # This is one policy. Another possibility is more computationally expensive and would pass only
129 | # valid arguments, silently dropping invalid ones. This can be implemented as a mutate handler.
130 | log.error(str(e).replace(endpoint.__name__, safe_name(endpoint)), extra=dict(
131 | request = id(context),
132 | endpoint = safe_name(endpoint),
133 | endpoint_args = args,
134 | endpoint_kw = kw,
135 | ))
136 |
137 | raise HTTPNotFound("Incorrect endpoint arguments: " + str(e))
138 |
139 |
140 | class ContextArgsExtension(ArgumentExtension):
141 | """Add the context as the first positional argument, possibly conditionally."""
142 |
143 | first = True
144 | provides = {'args.context'}
145 |
146 | def __init__(self, always=False):
147 | """Configure the conditions under which the context is added to endpoint positional arguments.
148 |
149 | When `always` is truthy the context is always included, otherwise it's only included for callables that are
150 | not bound methods.
151 | """
152 | self.always = always
153 |
154 | def mutate(self, context, endpoint, args, kw):
155 | if not self.always:
156 | # Instance methods were handed the context at class construction time via dispatch.
157 | # The `not isroutine` bit here catches callable instances, a la "index.html" handling.
158 | if not isroutine(endpoint) or (ismethod(endpoint) and getattr(endpoint, '__self__', None) is not None):
159 | return
160 |
161 | args.insert(0, context)
162 |
163 |
164 | class RemainderArgsExtension(ArgumentExtension):
165 | """Add any unprocessed path segments as positional arguments."""
166 |
167 | first = True
168 | needs = {'request'}
169 | uses = {'args.context'}
170 | provides = {'args', 'args.remainder'}
171 |
172 | def mutate(self, context, endpoint, args, kw):
173 | if not context.request.remainder:
174 | return
175 |
176 | args.extend(i for i in context.request.remainder if i)
177 |
178 |
179 | class QueryStringArgsExtension(ArgumentExtension):
180 | """Add query string arguments ("GET") as keyword arguments."""
181 |
182 | first = True
183 | needs = {'request'}
184 | provides = {'kwargs', 'kwargs.get'}
185 |
186 | def mutate(self, context, endpoint, args, kw):
187 | self._process_flat_kwargs(context.request.GET, kw)
188 |
189 |
190 | class FormEncodedKwargsExtension(ArgumentExtension):
191 | """Add form-encoded or MIME mmultipart ("POST") arguments as keyword arguments."""
192 |
193 | first = True
194 | needs = {'request'}
195 | uses = {'kwargs.get'} # Query string values must be processed first, to be overridden.
196 | provides = {'kwargs', 'kwargs.post'}
197 |
198 | def mutate(self, context, endpoint, args, kw):
199 | self._process_flat_kwargs(context.request.POST, kw)
200 |
201 |
202 | class JSONKwargsExtension(ArgumentExtension):
203 | """Add JSON-encoded arguments from the request body as keyword arguments."""
204 |
205 | first = True
206 | needs = {'request'}
207 | uses = {'kwargs.get'} # We override values defined in the query string.
208 | provides = {'kwargs', 'kwargs.json'}
209 |
210 | def mutate(self, context, endpoint, args, kw):
211 | if not context.request.content_type == 'application/json':
212 | return
213 |
214 | if not context.request.body:
215 | return
216 |
217 | self._process_rich_kwargs(context.request.json, kw)
218 |
219 |
220 |
--------------------------------------------------------------------------------
/docs/app/dycco.css:
--------------------------------------------------------------------------------
1 | /*--------------------- Layout and Typography ----------------------------*/
2 | body {
3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
4 | font-size: 15px;
5 | line-height: 22px;
6 | color: #252519;
7 | margin: 0;
8 | padding: 0 0 2em;
9 | }
10 | a {
11 | color: #261a3b;
12 | }
13 | a:visited {
14 | color: #261a3b;
15 | }
16 | p {
17 | margin: 0 0 15px 0;
18 | }
19 | h1, h2, h3, h4, h5, h6 {
20 | margin: 0px 0 15px 0;
21 | }
22 | h1 {
23 | margin-top: 40px;
24 | }
25 | #container {
26 | position: relative;
27 | }
28 | #background {
29 | position: fixed;
30 | top: 0; left: 525px; right: 0; bottom: 0;
31 | background: #f5f5ff;
32 | border-left: 1px solid #e5e5ee;
33 | z-index: -1;
34 | }
35 | #jump_to, #jump_page {
36 | background: white;
37 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;
38 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;
39 | font: 10px Arial;
40 | text-transform: uppercase;
41 | cursor: pointer;
42 | text-align: right;
43 | }
44 | #jump_to, #jump_wrapper {
45 | position: fixed;
46 | right: 0; top: 0;
47 | padding: 5px 10px;
48 | }
49 | #jump_wrapper {
50 | padding: 0;
51 | display: none;
52 | }
53 | #jump_to:hover #jump_wrapper {
54 | display: block;
55 | }
56 | #jump_page {
57 | padding: 5px 0 3px;
58 | margin: 0 0 25px 25px;
59 | }
60 | #jump_page .source {
61 | display: block;
62 | padding: 5px 10px;
63 | text-decoration: none;
64 | border-top: 1px solid #eee;
65 | }
66 | #jump_page .source:hover {
67 | background: #f5f5ff;
68 | }
69 | #jump_page .source:first-child {
70 | }
71 | table td {
72 | border: 0;
73 | outline: 0;
74 | }
75 | td.docs, th.docs {
76 | max-width: 450px;
77 | min-width: 450px;
78 | min-height: 5px;
79 | padding: 10px 25px 1px 50px;
80 | overflow-x: hidden;
81 | vertical-align: top;
82 | text-align: left;
83 | }
84 | .docs pre {
85 | margin: 15px 0 15px;
86 | padding-left: 15px;
87 | }
88 | .docs p tt, .docs p code {
89 | background: #f8f8ff;
90 | border: 1px solid #dedede;
91 | font-size: 12px;
92 | padding: 0 0.2em;
93 | }
94 | .pilwrap {
95 | position: relative;
96 | }
97 | .pilcrow {
98 | font: 12px Arial;
99 | text-decoration: none;
100 | color: #454545;
101 | position: absolute;
102 | top: 3px; left: -20px;
103 | padding: 1px 2px;
104 | opacity: 0;
105 | -webkit-transition: opacity 0.2s linear;
106 | }
107 | td.docs:hover .pilcrow {
108 | opacity: 1;
109 | }
110 | td.code, th.code {
111 | padding: 14px 15px 16px 25px;
112 | width: 100%;
113 | vertical-align: top;
114 | background: #f5f5ff;
115 | border-left: 1px solid #e5e5ee;
116 | }
117 | pre, tt, code {
118 | font-size: 12px; line-height: 18px;
119 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace;
120 | margin: 0; padding: 0;
121 | }
122 |
123 | /*---------------------- Syntax Highlighting -----------------------------*/
124 | td.linenos { background-color: #f0f0f0; padding-right: 10px; }
125 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }
126 | body .hll { background-color: #ffffcc }
127 | body .c { color: #408080; font-style: italic } /* Comment */
128 | body .err { border: 1px solid #FF0000 } /* Error */
129 | body .k { color: #954121 } /* Keyword */
130 | body .o { color: #666666 } /* Operator */
131 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
132 | body .cp { color: #BC7A00 } /* Comment.Preproc */
133 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */
134 | body .cs { color: #408080; font-style: italic } /* Comment.Special */
135 | body .gd { color: #A00000 } /* Generic.Deleted */
136 | body .ge { font-style: italic } /* Generic.Emph */
137 | body .gr { color: #FF0000 } /* Generic.Error */
138 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */
139 | body .gi { color: #00A000 } /* Generic.Inserted */
140 | body .go { color: #808080 } /* Generic.Output */
141 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
142 | body .gs { font-weight: bold } /* Generic.Strong */
143 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
144 | body .gt { color: #0040D0 } /* Generic.Traceback */
145 | body .kc { color: #954121 } /* Keyword.Constant */
146 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */
147 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */
148 | body .kp { color: #954121 } /* Keyword.Pseudo */
149 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */
150 | body .kt { color: #B00040 } /* Keyword.Type */
151 | body .m { color: #666666 } /* Literal.Number */
152 | body .s { color: #219161 } /* Literal.String */
153 | body .na { color: #7D9029 } /* Name.Attribute */
154 | body .nb { color: #954121 } /* Name.Builtin */
155 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */
156 | body .no { color: #880000 } /* Name.Constant */
157 | body .nd { color: #AA22FF } /* Name.Decorator */
158 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */
159 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
160 | body .nf { color: #0000FF } /* Name.Function */
161 | body .nl { color: #A0A000 } /* Name.Label */
162 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
163 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */
164 | body .nv { color: #19469D } /* Name.Variable */
165 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
166 | body .w { color: #bbbbbb } /* Text.Whitespace */
167 | body .mf { color: #666666 } /* Literal.Number.Float */
168 | body .mh { color: #666666 } /* Literal.Number.Hex */
169 | body .mi { color: #666666 } /* Literal.Number.Integer */
170 | body .mo { color: #666666 } /* Literal.Number.Oct */
171 | body .sb { color: #219161 } /* Literal.String.Backtick */
172 | body .sc { color: #219161 } /* Literal.String.Char */
173 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */
174 | body .s2 { color: #219161 } /* Literal.String.Double */
175 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
176 | body .sh { color: #219161 } /* Literal.String.Heredoc */
177 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
178 | body .sx { color: #954121 } /* Literal.String.Other */
179 | body .sr { color: #BB6688 } /* Literal.String.Regex */
180 | body .s1 { color: #219161 } /* Literal.String.Single */
181 | body .ss { color: #19469D } /* Literal.String.Symbol */
182 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */
183 | body .vc { color: #19469D } /* Name.Variable.Class */
184 | body .vg { color: #19469D } /* Name.Variable.Global */
185 | body .vi { color: #19469D } /* Name.Variable.Instance */
186 | body .il { color: #666666 } /* Literal.Number.Integer.Long */
187 |
188 | /* my customizations */
189 | footer {
190 | font-size: 12px;
191 | padding: 5px 50px;
192 | margin-top: 25px;
193 | background-color: #fff;
194 | border-top: 1px solid #e5e5ee;
195 | position: fixed;
196 | bottom: 0;
197 | left: 0;
198 | width: 100%;
199 | }
200 |
--------------------------------------------------------------------------------