Skip to content

Commit

Permalink
Merge pull request arskom#195 from plq/master
Browse files Browse the repository at this point in the history
Add Http Pattern support to WsgiApplication
  • Loading branch information
plq committed Oct 28, 2012
2 parents f4ea16b + 2396b00 commit 2f29352
Show file tree
Hide file tree
Showing 14 changed files with 521 additions and 35 deletions.
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -100,7 +100,7 @@ def finalize_options(self):
def run_tests(self):
print "running tests"
ret = 0
ret = call_pytest('interface','model','protocol','wsdl', 'test_*') or ret
ret = call_pytest('interface', 'model', 'protocol', 'test_*') or ret
ret = call_pytest('interop/test_httprpc.py') or ret
ret = call_pytest('interop/test_soap_client_http.py') or ret
ret = call_pytest('interop/test_soap_client_zeromq.py') or ret
Expand Down
10 changes: 9 additions & 1 deletion spyne/_base.py
Expand Up @@ -278,7 +278,7 @@ def __init__(self, function, in_message, out_message, doc,
is_callback=False, is_async=False, mtom=False, in_header=None,
out_header=None, faults=None,
port_type=None, no_ctx=False, udp=None, class_key=None,
aux=None):
aux=None, patterns=None):

self.__real_function = function
"""The original callable for the user code."""
Expand Down Expand Up @@ -343,6 +343,14 @@ def __init__(self, function, in_message, out_message, doc,
are ignored by the rpc layer.
"""

self.patterns = patterns
"""This list stores patterns which will match this callable using
various elements of the request protocol.
Currently, the only object supported here is the
:class:`spyne.protocol.http.HttpPattern` object.
"""

@property
def name(self):
"""The public name of the function. Equals to the type_name of the
Expand Down
11 changes: 10 additions & 1 deletion spyne/decorator.py
Expand Up @@ -200,6 +200,8 @@ def explain_method(*args, **kwargs):
_no_ctx = kparams.get('_no_ctx', True)
_udp = kparams.get('_udp', None)
_aux = kparams.get('_aux', None)
_pattern = kparams.get("_pattern",None)
_patterns = kparams.get("_patterns",[])

_faults = None
if ('_faults' in kparams) and ('_throws' in kparams):
Expand All @@ -219,11 +221,18 @@ def explain_method(*args, **kwargs):

doc = getattr(f, '__doc__')

if _pattern is not None and _patterns != []:
raise ValueError("only one of '_pattern' and '__patterns' "
"arguments should be given")

if _pattern is not None:
_patterns = [_pattern]

retval = MethodDescriptor(f,
in_message, out_message, doc, _is_callback, _is_async,
_mtom, _in_header, _out_header, _faults,
port_type=_port_type, no_ctx=_no_ctx, udp=_udp,
class_key=function_name, aux=_aux)
class_key=function_name, aux=_aux, patterns=_patterns)

return retval

Expand Down
1 change: 0 additions & 1 deletion spyne/interface/_base.py
Expand Up @@ -30,7 +30,6 @@
from spyne.const.suffix import RESPONSE_SUFFIX

from spyne.model import ModelBase
from spyne.model.complex import Alias
from spyne.model.complex import ComplexModelBase


Expand Down
3 changes: 2 additions & 1 deletion spyne/interface/xml_schema/model.py
Expand Up @@ -61,7 +61,8 @@ def complex_add(document, cls):
complex_type = etree.Element("{%s}complexType" % _ns_xsd)
complex_type.set('name', cls.get_type_name())

if cls.Annotations.doc != '' or cls.Annotations.appinfo != None or cls.Annotations.__use_parent_doc__:
if cls.Annotations.doc != '' or cls.Annotations.appinfo != None or \
cls.Annotations.__use_parent_doc__:
annotation = etree.SubElement(complex_type, "{%s}annotation" % _ns_xsd)
if cls.Annotations.doc != '' or cls.Annotations.__use_parent_doc__:
doc = etree.SubElement(annotation, "{%s}documentation" % _ns_xsd)
Expand Down
40 changes: 35 additions & 5 deletions spyne/protocol/http.py
Expand Up @@ -40,13 +40,15 @@

from spyne.protocol.dictobj import DictObject


def get_stream_factory(dir=None, delete=True):
def stream_factory(total_content_length, filename, content_type,
content_length=None):
if total_content_length >= SWAP_DATA_TO_FILE_THRESHOLD or delete == False:
if total_content_length >= SWAP_DATA_TO_FILE_THRESHOLD or \
delete == False:
if delete == False:
retval = tempfile.NamedTemporaryFile('wb+', dir=dir, delete=delete) # You need python >= 2.6 for this.
# You need python >= 2.6 for this.
retval = tempfile.NamedTemporaryFile('wb+', dir=dir,
delete=delete)
else:
retval = tempfile.NamedTemporaryFile('wb+', dir=dir)
else:
Expand Down Expand Up @@ -76,9 +78,11 @@ def get_tmp_delete_on_close(self):

def set_tmp_delete_on_close(self, val):
self.__tmp_delete_on_close = val
self.stream_factory = get_stream_factory(self.tmp_dir, self.__tmp_delete_on_close)
self.stream_factory = get_stream_factory(self.tmp_dir,
self.__tmp_delete_on_close)

tmp_delete_on_close = property(get_tmp_delete_on_close, set_tmp_delete_on_close)
tmp_delete_on_close = property(get_tmp_delete_on_close,
set_tmp_delete_on_close)

def set_validator(self, validator):
if validator == 'soft' or validator is self.SOFT_VALIDATION:
Expand Down Expand Up @@ -151,3 +155,29 @@ def serialize(self, ctx, message):

def create_out_string(self, ctx, out_string_encoding='utf8'):
ctx.out_string = ctx.out_document


class HttpPattern(object):
"""Experimental. Stay away.
:param address: Address pattern
:param verb: HTTP Verb pattern
:param host: HTTP "Host:" header pattern
"""

def __init__(self, address, verb=None, host=None, endpoint=None):
self.address = address
self.host = host
self.verb = verb
self.endpoint = endpoint

def as_werkzeug_rule(self):
from werkzeug.routing import Rule
from spyne.util.invregex import invregex

methods = None
if self.verb is not None:
methods = invregex(self.verb)

return Rule(self.address, host=self.host, endpoint=self.endpoint,
methods=methods)
96 changes: 88 additions & 8 deletions spyne/server/wsgi.py
Expand Up @@ -37,12 +37,14 @@
try:
from werkzeug.formparser import parse_form_data
except ImportError:
pass
parse_form_data = None

from spyne.server.http import HttpMethodContext
from spyne.server.http import HttpTransportContext

from spyne.error import RequestTooLongError

from spyne.protocol.http import HttpPattern
from spyne.protocol.soap.mime import apply_mtom
from spyne.util import reconstruct_url
from spyne.server.http import HttpBase
Expand Down Expand Up @@ -118,6 +120,7 @@ class WsgiApplication(HttpBase):
called both from success and error cases.
'''


def __init__(self, app, chunked=True):
HttpBase.__init__(self, app, chunked)

Expand All @@ -129,6 +132,29 @@ def __init__(self, app, chunked=True):
self._mtx_build_interface_document = threading.Lock()
self._wsdl = None

# Initialize HTTP Patterns
self._http_patterns = None
self._map_adapter = None
self._mtx_build_map_adapter = threading.Lock()

for k,v in self.app.interface.service_method_map.items():
p_service_class, p_method_descriptor = v[0]
for p in p_method_descriptor.patterns:
if isinstance(p, HttpPattern):
r = p.as_werkzeug_rule()

# We do this here because we don't want to import
# Werkzeug until the last moment.
if self._http_patterns is None:
from werkzeug.routing import Map
self._http_patterns = Map()

self._http_patterns.add(r)

@property
def has_patterns(self):
return self._http_patterns is not None

def __call__(self, req_env, start_response, wsgi_url=None):
'''This method conforms to the WSGI spec for callable wsgi applications
(PEP 333). It looks in environ['wsgi.input'] for a fully formed rpc
Expand Down Expand Up @@ -294,14 +320,14 @@ def handle_rpc(self, req_env, start_response):
# a better way of having generator functions execute until first yield,
# just let us know.
try:
len(p_ctx.out_string) # iterator?
# nope
len(p_ctx.out_string) # generator?

# nope
p_ctx.transport.resp_headers['Content-Length'] = \
str(sum([len(a) for a in p_ctx.out_string]))
str(sum([len(a) for a in p_ctx.out_string]))

start_response(p_ctx.transport.resp_code,
p_ctx.transport.resp_headers.items())
p_ctx.transport.resp_headers.items())

retval = itertools.chain(p_ctx.out_string, self.__finalize(p_ctx))

Expand Down Expand Up @@ -371,20 +397,74 @@ def __wsgi_input_to_iterable(self, http_env):

yield data

@staticmethod
def decompose_incoming_envelope(prot, ctx, message):
def generate_map_adapter(self, ctx):
try:
self._mtx_build_map_adapter.acquire()
if self._map_adapter is None:
# If url map is not binded before, binds url_map
req_env = ctx.transport.req_env
self._map_adapter = self._http_patterns.bind(
req_env['SERVER_NAME'], "/")

for k,v in ctx.app.interface.service_method_map.items():
#Compiles url patterns
p_service_class, p_method_descriptor = v[0]
for r in self._http_patterns.iter_rules():
params = {}
if r.endpoint == k:
for pk, pv in p_method_descriptor.in_message.\
_type_info.items():
if pk in r.rule:
from spyne.model.primitive import String
from spyne.model.primitive import Unicode
from spyne.model.primitive import Decimal

if issubclass(pv, Unicode):
params[pk] = ""
elif issubclass(pv, Decimal):
params[pk] = 0

self._map_adapter.build(r.endpoint, params)

finally:
self._mtx_build_map_adapter.release()


def decompose_incoming_envelope(self, prot, ctx, message):
"""This function is only called by the HttpRpc protocol to have the wsgi
environment parsed into ``ctx.in_body_doc`` and ``ctx.in_header_doc``.
"""
if self.has_patterns:
from werkzeug.exceptions import NotFound
if self._map_adapter is None:
self.generate_map_adapter(ctx)

ctx.method_request_string = '{%s}%s' % (prot.app.interface.get_tns(),
try:
#If PATH_INFO matches a url, Set method_request_string to mrs
mrs, params = self._map_adapter.match(ctx.in_document["PATH_INFO"],
ctx.in_document["REQUEST_METHOD"])
ctx.method_request_string = mrs

except NotFound:
# Else set method_request_string normally
params = {}
ctx.method_request_string = '{%s}%s' % (prot.app.interface.get_tns(),
ctx.in_document['PATH_INFO'].split('/')[-1])
else:
params = {}
ctx.method_request_string = '{%s}%s' % (prot.app.interface.get_tns(),
ctx.in_document['PATH_INFO'].split('/')[-1])

logger.debug("%sMethod name: %r%s" % (LIGHT_GREEN,
ctx.method_request_string, END_COLOR))

ctx.in_header_doc = _get_http_headers(ctx.in_document)
ctx.in_body_doc = parse_qs(ctx.in_document['QUERY_STRING'])
for k,v in params.items():
if k in ctx.in_body_doc:
ctx.in_body_doc[k].append(v)
else:
ctx.in_body_doc[k] = [v]

if ctx.in_document['REQUEST_METHOD'].upper() in ('POST', 'PUT', 'PATCH'):
stream, form, files = parse_form_data(ctx.in_document,
Expand Down
3 changes: 3 additions & 0 deletions spyne/service.py
Expand Up @@ -55,6 +55,9 @@ def __init__(self, cls_name, cls_bases, cls_dict):
else:
self.__has_aux_methods = True

for p in descriptor.patterns:
p.endpoint = k

def __get_base_event_handlers(self, cls_bases):
handlers = {}

Expand Down
4 changes: 4 additions & 0 deletions spyne/test/protocol/test_html_microformat.py
Expand Up @@ -50,6 +50,7 @@ def some_call(s):
'QUERY_STRING': 's=s',
'PATH_INFO': '/some_call',
'REQUEST_METHOD': 'GET',
'SERVER_NAME': 'localhost',
}, 'some-content-type')

ctx, = server.generate_contexts(initial_ctx)
Expand Down Expand Up @@ -77,6 +78,7 @@ def some_call():
'QUERY_STRING': '',
'PATH_INFO': '/some_call',
'REQUEST_METHOD': 'GET',
'SERVER_NAME': 'localhost',
}, 'some-content-type')

ctx, = server.generate_contexts(initial_ctx)
Expand Down Expand Up @@ -109,6 +111,7 @@ def some_call(ccm):
'QUERY_STRING': 'ccm_c_s=abc&ccm_c_i=123&ccm_i=456&ccm_s=def',
'PATH_INFO': '/some_call',
'REQUEST_METHOD': 'GET',
'SERVER_NAME': 'localhost',
}, 'some-content-type')

ctx, = server.generate_contexts(initial_ctx)
Expand Down Expand Up @@ -171,6 +174,7 @@ def some_call(s):
'QUERY_STRING': 's=1&s=2',
'PATH_INFO': '/some_call',
'REQUEST_METHOD': 'GET',
'SERVER_NAME': 'localhost',
}, 'some-content-type')

ctx, = server.generate_contexts(initial_ctx)
Expand Down

0 comments on commit 2f29352

Please sign in to comment.