Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Split into submodules, add memcached support

  • Loading branch information...
commit 7cd411c0a2944f15ccda258dc1e15d5f5ee885b6 1 parent 61dc346
@alekstorm authored
View
3  .gitignore
@@ -0,0 +1,3 @@
+build/
+dist/
+*.egg-info/
View
3  README.md
@@ -0,0 +1,3 @@
+# Vortex #
+
+Vortex is an experimental resource-based Python webserver based on Tornado's IOLoop.
View
3  setup.py
@@ -19,7 +19,8 @@
distutils.core.setup(
name="vortex",
- py_modules = ["vortex"],
+ version="0.0.3",
+ packages = ["vortex"],
author="Alek Storm",
author_email="alek.storm@gmail.com",
url="http://alekstorm.github.com/vortex",
View
17 test.py
@@ -1,17 +0,0 @@
-from tornado.httpserver import HTTPServer
-from tornado.ioloop import IOLoop
-from vortex import *
-
-class ArgResource(Resource):
- def get(self, request, a, b='default'):
- return 'Success: a=%s, b=%s' % (a, b)
-
-if __name__ == '__main__':
- app = Application({
- '': lambda request: 'Hello World!',
- 'static': StaticDirectoryResource(os.path.join(os.path.dirname(__file__), 'static')),
- 'json': JSONResource({'a': ['b', 1], 'c': {'d': 2}}),
- 'args': ArgResource(),
- })
- HTTPServer(app).listen(port=3000)
- IOLoop.instance().start()
View
0  static/test.html → test/static/test.html
File renamed without changes
View
36 test/test.py
@@ -0,0 +1,36 @@
+from memcache import Client
+import time
+from tornado.httpserver import HTTPServer
+from tornado.ioloop import IOLoop
+from vortex.app import Application
+from vortex.memcached import Memcacher, memcached
+from vortex.resource import *
+
+class ArgResource(Resource):
+ def get(self, request, a, b='default'):
+ return 'Success: a=%s, b=%s' % (a, b)
+
+@memcached
+class MemcachedResource(Resource):
+ def __getitem__(self, name):
+ return MemcachedSubResource()
+
+class MemcachedSubResource(Resource):
+ def __init__(self):
+ Resource.__init__(self)
+ time.sleep(2)
+ self.expensive = 'This took a long time to compute'
+
+ def get(self, request):
+ return self.expensive
+
+if __name__ == '__main__':
+ app = Application(Memcacher(Client(['127.0.0.1:11211']), {
+ '': lambda request: 'Hello World!',
+ 'static': StaticDirectoryResource(os.path.join(os.path.dirname(__file__), 'static')),
+ 'json': JSONResource({'a': ['b', 1], 'c': {'d': 2}}),
+ 'args': ArgResource(),
+ 'memcached': MemcachedResource(),
+ }))
+ HTTPServer(app).listen(port=3000)
+ IOLoop.instance().start()
View
250 vortex.py
@@ -1,250 +0,0 @@
-import Cookie
-import datetime
-import email.utils
-import hashlib
-import httplib
-import inspect
-import json
-import mimetypes
-import os.path
-import time
-from tornado.escape import utf8
-import traceback
-import urllib
-
-SAFE_METHODS = ('GET', 'HEAD')
-
-class HTTPResponse(object):
- def __init__(self, status_code=httplib.OK, reason=None, entity='', version='HTTP/1.1', headers=None, cookies=None):
- self.status_code = status_code
- self.reason = reason
- self.entity = entity
- self.version = version
- self.headers = headers or {}
- self.cookies = Cookie.SimpleCookie()
- for key, value in (cookies or {}).iteritems():
- if isinstance(value, dict):
- self.cookies[key] = value['value']
- for name, morsel_attr in value:
- self.cookies[key][name] = morsel_attr
- else:
- self.cookies[key] = value
-
- def __str__(self):
- lines = [utf8(self.version + b' ' + str(self.status_code) + b' ' + (self.reason or httplib.responses[self.status_code]))]
- self.headers.setdefault('Content-Length', str(len(self.entity)))
- self.headers.setdefault('Content-Type', 'text/html')
- for name, values in self.headers.iteritems():
- lines.extend([utf8(name) + b': ' + utf8(value) for value in (values if isinstance(values, list) else [values])])
- for cookie in self.cookies.itervalues():
- lines.append(str(cookie))
- return b'\r\n'.join(lines) + b'\r\n\r\n' + self.entity
-
-
-class HTTPNoContentResponse(HTTPResponse):
- def __init__(self, cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.NO_CONTENT, cookies=cookies)
-
-
-class HTTPFoundResponse(HTTPResponse):
- def __init__(self, location, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.FOUND, headers={'Location': location}, entity=entity, cookies=cookies)
-
-
-class HTTPNotModifiedResponse(HTTPResponse):
- def __init__(self, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.NOT_MODIFIED, entity=entity, cookies=cookies)
-
-
-class HTTPNotFoundResponse(HTTPResponse):
- def __init__(self, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.NOT_FOUND, entity=entity, cookies=cookies)
-
-
-class HTTPBadRequestResponse(HTTPResponse):
- def __init__(self, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.BAD_REQUEST, entity=entity, cookies=cookies)
-
-
-class HTTPUnauthorizedResponse(HTTPResponse):
- def __init__(self, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.UNAUTHORIZED, entity=entity, cookies=cookies)
-
-
-class HTTPForbiddenResponse(HTTPResponse):
- def __init__(self, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.FORBIDDEN, entity=entity, cookies=cookies)
-
-
-class HTTPMethodNotAllowedResponse(HTTPResponse):
- def __init__(self, allowed, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.METHOD_NOT_ALLOWED, headers={'Allowed': allowed}, entity=entity, cookies=cookies)
-
-
-class HTTPNotImplementedResponse(HTTPResponse):
- def __init__(self, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.NOT_IMPLEMENTED, entity=entity, cookies=cookies)
-
-
-class HTTPInternalServerErrorResponse(HTTPResponse):
- def __init__(self, entity='', cookies=None):
- HTTPResponse.__init__(self, status_code=httplib.INTERNAL_SERVER_ERROR, entity=entity, cookies=cookies)
-
-
-class Resource(object):
- SUPPORTED_METHODS = ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE']
-
- def __init__(self, sub_resources=None):
- self.supported_methods = [method for method in self.SUPPORTED_METHODS if hasattr(self, method.lower())]
- self.sub_resources = sub_resources or {}
-
- def __call__(self, request):
- kwargs = dict([(key, value[0]) for key, value in request.arguments.iteritems()])
- if request.method in self.supported_methods:
- method_name = request.method.lower()
- elif request.method == 'HEAD' and 'GET' in self.supported_methods:
- method_name = 'get'
- elif request.method.upper() in self.SUPPORTED_METHODS:
- return HTTPMethodNotAllowedResponse(allowed=self.supported_methods)
- else:
- return HTTPNotImplementedResponse()
-
- method = getattr(self, method_name)
- try:
- return method(request, **kwargs)
- except TypeError as err:
- argspec = inspect.getargspec(method)
- args = argspec.args[2:]
- keywords = set(kwargs.keys())
- missing = set(args[:-len(argspec.defaults)] if argspec.defaults else args) - keywords
- if len(missing) > 0:
- return HTTPBadRequestResponse(entity='Missing arguments: '+' '.join(missing))
- invalid = keywords - set(args)
- if not argspec.keywords and len(invalid) > 0:
- return HTTPBadRequestResponse(entity='Unexpected arguments: '+' '.join(invalid))
- raise
-
- def __getitem__(self, name):
- return self.sub_resources[name]
-
-
-class MutableResource(Resource):
- def __setitem__(self, name, value):
- self.sub_resources[name] = value
-
- def __delitem__(self, name):
- del self.sub_resources[name]
-
-class StaticFileResource(Resource):
- CACHE_MAX_AGE = 60*60*24*365*10 # 10 years in seconds
-
- def __init__(self, path):
- Resource.__init__(self)
- self.path = path
-
- def head(self, request, **kwargs):
- return self.get(request, **kwargs)
-
- def get(self, request, v=None):
- if not os.path.exists(self.path):
- return HTTPNotFoundResponse()
- if not os.path.isfile(self.path):
- return HTTPForbiddenResponse(entity='%s is not a file' % self.path)
-
- # Don't send the result if the content has not been modified since the If-Modified-Since
- modified = os.stat(self.path).st_mtime
- if 'If-Modified-Since' in request.headers and time.mktime(email.utils.parsedate(request.headers['If-Modified-Since'])) >= modified:
- return HTTPNotModifiedResponse()
-
- headers = {'Last-Modified': str(email.utils.formatdate(modified))}
-
- mimetype, encoding = mimetypes.guess_type(self.path)
- if mimetype:
- headers['Content-Type'] = mimetype
-
- cache_time = self.CACHE_MAX_AGE if v else 0
-
- if cache_time > 0:
- headers['Expires'] = str(datetime.datetime.utcnow() + datetime.timedelta(seconds=cache_time))
- headers['Cache-Control'] = 'max-age=' + str(cache_time)
- else:
- headers['Cache-Control'] = 'public'
-
- if request.method == 'HEAD':
- return HTTPResponse(headers=headers)
-
- file = open(self.path, 'rb')
- try:
- contents = HTTPResponse(entity=file.read(), headers=headers)
- finally:
- file.close()
- return contents
-
-
-class StaticDirectoryResource(StaticFileResource):
- def __getitem__(self, name):
- if not os.path.isdir(self.path):
- return HTTPForbiddenResponse(entity='%s is not a directory' % path)
- return StaticDirectoryResource(os.path.join(self.path, name))
-
-
-class JSONResource(Resource):
- def __getitem__(self, name):
- return JSONResource(Resource.__getitem__(self, name if not isinstance(self.sub_resources, list) else int(name)))
-
- def get(self, request):
- return json.dumps(self.sub_resources)
-
-
-class TraceResource(Resource):
- def trace(self, request, **kwargs):
- return str(request) # FIXME
-
-
-class Application(object):
- def __init__(self, resource=None):
- self.resource = resource
-
- def __call__(self, request):
- try:
- resource = self.resource
- response = None
- for part in request.path.split('/')[1:]:
- not_found = False
- if resource is not None and hasattr(resource, '__getitem__'):
- try:
- resource = resource[urllib.unquote(part)]
- except KeyError:
- not_found = True
- else:
- not_found = True
- if not_found:
- response = HTTPNotFoundResponse()
- break
- if response is None:
- response = resource(request) if hasattr(resource, '__call__') else HTTPMethodNotAllowedResponse(allowed=[])
- if response is None:
- response = HTTPNoContentResponse()
- elif isinstance(response, basestring):
- response = HTTPResponse(entity=response)
- elif isinstance(response, dict):
- response = HTTPResponse(entity=json.dumps(response))
- response.headers.setdefault('Content-Type', 'application/json')
-
- if response.status_code == httplib.OK and request.method in SAFE_METHODS:
- etag = hashlib.sha1(response.entity).hexdigest()
- inm = request.headers.get('If-None-Match')
- if inm and inm.find(etag) != -1:
- response = HTTPNotModifiedResponse()
- else:
- response.headers.setdefault('Etag', etag)
-
- if request.method == 'HEAD':
- response.entity = ''
- except:
- response = HTTPInternalServerErrorResponse(entity=traceback.format_exc())
- if response.status_code in (httplib.INTERNAL_SERVER_ERROR, httplib.BAD_REQUEST):
- print str(response)
- request.write(str(response))
- request.finish()
- return response
View
1  vortex/__init__.py
@@ -0,0 +1 @@
+__all__ = ['app', 'memcached', 'resource', 'response']
View
59 vortex/app.py
@@ -0,0 +1,59 @@
+import hashlib
+import httplib
+import json
+import traceback
+import urllib
+
+from vortex.response import *
+
+def coerce_response(response):
+ if response is None:
+ response = HTTPNoContentResponse()
+ elif isinstance(response, basestring):
+ response = HTTPResponse(entity=response)
+ elif isinstance(response, dict):
+ response = HTTPResponse(entity=json.dumps(response))
+ response.headers.setdefault('Content-Type', 'application/json')
+ return response
+
+
+class Application(object):
+ def __init__(self, resource=None):
+ self.resource = resource
+
+ def __call__(self, request):
+ try:
+ resource = self.resource
+ response = None
+ for part in request.path.split('/')[1:]:
+ not_found = False
+ if resource is not None and hasattr(resource, '__getitem__'):
+ try:
+ resource = resource[urllib.unquote(part)]
+ except KeyError:
+ not_found = True
+ else:
+ not_found = True
+ if not_found:
+ response = HTTPNotFoundResponse()
+ break
+ if response is None:
+ response = coerce_response(resource(request)) if hasattr(resource, '__call__') else HTTPMethodNotAllowedResponse(allowed=[])
+
+ if response.status_code == httplib.OK and request.method in SAFE_METHODS:
+ etag = hashlib.sha1(response.entity).hexdigest()
+ inm = request.headers.get('If-None-Match')
+ if inm and inm.find(etag) != -1:
+ response = HTTPNotModifiedResponse()
+ else:
+ response.headers.setdefault('Etag', etag)
+
+ if request.method == 'HEAD':
+ response.entity = ''
+ except:
+ response = HTTPInternalServerErrorResponse(entity=traceback.format_exc())
+ if response.status_code in (httplib.INTERNAL_SERVER_ERROR, httplib.BAD_REQUEST):
+ print str(response)
+ request.write(str(response))
+ request.finish()
+ return response
View
43 vortex/memcached.py
@@ -0,0 +1,43 @@
+class _Accumulator(object):
+ def __init__(self, mc, root):
+ self.mc = mc
+ self.path = ''
+ self.parent = root
+
+ def __getitem__(self, name):
+ self.path += '/'+name
+ if hasattr(self.parent, '_memcached'):
+ child = self.mc.get(self.path)
+ if child is None:
+ child = self.parent[name]
+ self.mc.set(self.path, child)
+ child._memcached_path = self.path
+ else:
+ child = self.parent[name]
+ self.parent = child
+ return self
+
+ def __call__(self, *args, **kwargs):
+ return self.parent(*args, **kwargs)
+
+class Memcacher(object):
+ def __init__(self, mc, root):
+ self.mc = mc
+ self.root = root
+
+ def persistent_id(obj):
+ if hasattr(obj, '_memcached'):
+ return obj._memcached_path
+ return None
+ self.mc.persistent_id = persistent_id
+
+ def persistent_load(path):
+ return self.mc.get(path)
+ self.mc.persistent_load = persistent_load
+
+ def __getitem__(self, name):
+ return _Accumulator(self.mc, self.root)[name]
+
+def memcached(cls):
+ cls._memcached = True
+ return cls
View
142 vortex/resource.py
@@ -0,0 +1,142 @@
+import datetime
+import email.utils
+import inspect
+import json
+import mimetypes
+import os.path
+import time
+
+from vortex.response import *
+
+class Resource(object):
+ SUPPORTED_METHODS = set(['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE'])
+
+ def __init__(self):
+ self.supported_methods = [method for method in self.SUPPORTED_METHODS if hasattr(self, method.lower())]
+
+ def __call__(self, request):
+ kwargs = dict([(key, value[0]) for key, value in request.arguments.iteritems()])
+ if request.method in self.supported_methods:
+ method_name = request.method.lower()
+ elif request.method == 'HEAD' and 'GET' in self.supported_methods:
+ method_name = 'get'
+ elif request.method.upper() in self.SUPPORTED_METHODS:
+ return HTTPMethodNotAllowedResponse(allowed=self.supported_methods)
+ else:
+ return HTTPNotImplementedResponse()
+
+ method = getattr(self, method_name)
+ try:
+ return method(request, **kwargs)
+ except TypeError as err:
+ argspec = inspect.getargspec(method)
+ args = argspec.args[2:]
+ keywords = set(kwargs.keys())
+ missing = set(args[:-len(argspec.defaults)] if argspec.defaults else args) - keywords
+ if len(missing) > 0:
+ return HTTPBadRequestResponse(entity='Missing arguments: '+' '.join(missing))
+ invalid = keywords - set(args)
+ if not argspec.keywords and len(invalid) > 0:
+ return HTTPBadRequestResponse(entity='Unexpected arguments: '+' '.join(invalid))
+ raise
+
+
+class DictResource(Resource):
+ def __init__(self, sub_resources=None):
+ Resource.__init__(self)
+ self.sub_resources = sub_resources or {}
+
+ def __getitem__(self, name):
+ return self.sub_resources[name]
+
+
+class MutableDictResource(DictResource):
+ def __setitem__(self, name, value):
+ self.sub_resources[name] = value
+
+ def __delitem__(self, name):
+ del self.sub_resources[name]
+
+
+class StaticFileResource(Resource):
+ CACHE_MAX_AGE = 60*60*24*365*10 # 10 years in seconds
+
+ def __init__(self, path):
+ Resource.__init__(self)
+ self.path = path
+
+ def head(self, request, **kwargs):
+ return self.get(request, **kwargs)
+
+ def get(self, request, v=None):
+ if not os.path.exists(self.path):
+ return HTTPNotFoundResponse()
+ if not os.path.isfile(self.path):
+ return HTTPForbiddenResponse(entity='%s is not a file' % self.path)
+
+ # Don't send the result if the content has not been modified since the If-Modified-Since
+ modified = os.stat(self.path).st_mtime
+ if 'If-Modified-Since' in request.headers and time.mktime(email.utils.parsedate(request.headers['If-Modified-Since'])) >= modified:
+ return HTTPNotModifiedResponse()
+
+ headers = {'Last-Modified': str(email.utils.formatdate(modified))}
+
+ mimetype, encoding = mimetypes.guess_type(self.path)
+ if mimetype:
+ headers['Content-Type'] = mimetype
+
+ cache_time = self.CACHE_MAX_AGE if v else 0
+
+ if cache_time > 0:
+ headers['Expires'] = str(datetime.datetime.utcnow() + datetime.timedelta(seconds=cache_time))
+ headers['Cache-Control'] = 'max-age=' + str(cache_time)
+ else:
+ headers['Cache-Control'] = 'public'
+
+ if request.method == 'HEAD':
+ return HTTPResponse(headers=headers)
+
+ file = open(self.path, 'rb')
+ try:
+ contents = HTTPResponse(entity=file.read(), headers=headers)
+ finally:
+ file.close()
+ return contents
+
+
+class StaticDirectoryResource(StaticFileResource):
+ def __getitem__(self, name):
+ if not os.path.isdir(self.path):
+ return HTTPForbiddenResponse(entity='%s is not a directory' % self.path)
+ return StaticDirectoryResource(os.path.join(self.path, name))
+
+
+class JSONResource(DictResource):
+ def __getitem__(self, name):
+ return JSONResource(DictResource.__getitem__(self, name if not isinstance(self.sub_resources, list) else int(name)))
+
+ def get(self, request, callback=None):
+ response = HTTPResponse(entity=json.dumps(self.sub_resources))
+ if callback is not None:
+ response.entity = callback+'('+response.entity+');'
+ response.headers['Content-Type'] = 'application/json-p'
+ return response
+
+
+class TraceResource(Resource):
+ def trace(self, request, **kwargs):
+ return str(request) # FIXME
+
+
+class LazyResource(Resource):
+ def __init__(self, lazy_resources=None):
+ self.lazy_resources = lazy_resources or {}
+ self.loaded_resources = {}
+
+ def __getitem__(self, name):
+ if name not in self.loaded_resources:
+ value = self.lazy_resources[name]()
+ self.loaded_resources[name] = value
+ else:
+ value = self.loaded_resources[name]
+ return value
View
82 vortex/response.py
@@ -0,0 +1,82 @@
+import Cookie
+import httplib
+from tornado.escape import utf8
+
+SAFE_METHODS = ('GET', 'HEAD')
+
+class HTTPResponse(object):
+ def __init__(self, status_code=httplib.OK, reason=None, entity='', version='HTTP/1.1', headers=None, cookies=None):
+ self.status_code = status_code
+ self.reason = reason
+ self.entity = entity
+ self.version = version
+ self.headers = headers or {}
+ self.cookies = Cookie.SimpleCookie()
+ for key, value in (cookies or {}).iteritems():
+ if isinstance(value, dict):
+ self.cookies[key] = value['value']
+ for name, morsel_attr in value:
+ self.cookies[key][name] = morsel_attr
+ else:
+ self.cookies[key] = value
+
+ def __str__(self):
+ lines = [utf8(self.version + b' ' + str(self.status_code) + b' ' + (self.reason or httplib.responses[self.status_code]))]
+ self.headers.setdefault('Content-Length', str(len(self.entity)))
+ self.headers.setdefault('Content-Type', 'text/html')
+ for name, values in self.headers.iteritems():
+ lines.extend([utf8(name) + b': ' + utf8(value) for value in (values if isinstance(values, list) else [values])])
+ for cookie in self.cookies.itervalues():
+ lines.append(str(cookie))
+ return b'\r\n'.join(lines) + b'\r\n\r\n' + self.entity
+
+
+class HTTPNoContentResponse(HTTPResponse):
+ def __init__(self, cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.NO_CONTENT, cookies=cookies)
+
+
+class HTTPFoundResponse(HTTPResponse):
+ def __init__(self, location, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.FOUND, headers={'Location': location}, entity=entity, cookies=cookies)
+
+
+class HTTPNotModifiedResponse(HTTPResponse):
+ def __init__(self, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.NOT_MODIFIED, entity=entity, cookies=cookies)
+
+
+class HTTPNotFoundResponse(HTTPResponse):
+ def __init__(self, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.NOT_FOUND, entity=entity, cookies=cookies)
+
+
+class HTTPBadRequestResponse(HTTPResponse):
+ def __init__(self, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.BAD_REQUEST, entity=entity, cookies=cookies)
+
+
+class HTTPUnauthorizedResponse(HTTPResponse):
+ def __init__(self, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.UNAUTHORIZED, entity=entity, cookies=cookies)
+
+
+class HTTPForbiddenResponse(HTTPResponse):
+ def __init__(self, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.FORBIDDEN, entity=entity, cookies=cookies)
+
+
+class HTTPMethodNotAllowedResponse(HTTPResponse):
+ def __init__(self, allowed, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.METHOD_NOT_ALLOWED, headers={'Allowed': allowed}, entity=entity, cookies=cookies)
+
+
+class HTTPNotImplementedResponse(HTTPResponse):
+ def __init__(self, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.NOT_IMPLEMENTED, entity=entity, cookies=cookies)
+
+
+class HTTPInternalServerErrorResponse(HTTPResponse):
+ def __init__(self, entity='', cookies=None):
+ HTTPResponse.__init__(self, status_code=httplib.INTERNAL_SERVER_ERROR, entity=entity, cookies=cookies)
+
Please sign in to comment.
Something went wrong with that request. Please try again.