Skip to content

Commit

Permalink
implemented a new REST protocol, added some tests checking that it wo…
Browse files Browse the repository at this point in the history
…rks along with the SOAP one, issue #29
  • Loading branch information
stepank committed Jan 20, 2013
1 parent 193b2c4 commit 0942a48
Show file tree
Hide file tree
Showing 13 changed files with 250 additions and 47 deletions.
3 changes: 2 additions & 1 deletion examples/api_settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pyws.protocols import GetProtocol, JsonProtocol
from pyws.protocols import GetProtocol, JsonProtocol, RestProtocol

from authenticate import authenticate, soap_headers_schema

Expand All @@ -7,6 +7,7 @@
PROTOCOLS = (
GetProtocol(),
JsonProtocol(),
RestProtocol(),
)

SOAP_PROTOCOL_PARAMS = (
Expand Down
36 changes: 35 additions & 1 deletion examples/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pyws.functions.args import DictOf
from pyws.functions.register import register
from pyws.protocols import url


# = add simple ================================================================
Expand All @@ -18,7 +19,7 @@ def add_simple(a, b):
def flip_boolean(b):
return b ^ True

# = say hello ================================================================
# = say hello =================================================================

@register('say_hello', needs_context=True)
def say_hello(context=None):
Expand Down Expand Up @@ -145,3 +146,36 @@ def raises_exception(name):
this function will always fail
"""
raise HelloError(name)


# = REST protocol related =====================================================

@register(
['create_item', url('put', 'some/item')],
return_type=int, args=((str, 0), ))
def create_item(name):
return int(name[4:])

@register(
['get_item', url('get', 'some/item/(?P<id>[\d]+)')],
return_type=str, args=((int, 0), ))
def get_item(id):
return 'item%s' % id

@register(
['get_items', url('get', 'some/item')],
return_type=[str])
def get_items():
return ['item%s' % i for i in range(10)]

@register(
['update_item', url('post', 'some/item/(?P<id>[\d]+)')],
return_type=str, args=((int, 0), (str, '')))
def update_item(id, name):
return name

@register(
['delete_item', url('delete', 'some/item/(?P<id>[\d]+)')],
return_type=bool, args=((int, 0), ))
def delete_item(id):
return True
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
extra_requires += ['simplejson']

if 'develop' in sys.argv:
extra_requires += ['unittest2', 'suds']
extra_requires += ['unittest2', 'suds', 'requests']

setup(
name='pyws',
Expand Down
36 changes: 27 additions & 9 deletions src/pyws/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from inspect import getargspec

from pyws.errors import ConfigurationError
from pyws.functions.args import DictOf, TypeFactory
from pyws.protocols import NameRoute, Route
from pyws.utils import cached_property

__all__ = ('Function', 'NativeFunctionAdapter', )
Expand All @@ -14,8 +16,8 @@ class Function(object):
values validation.
"""

#: the name of the function;
name = None
#: the route of the function;
route = None
#: the documentation of the function
documentation = None
#: the type of values returning by the function (standard form only), see
Expand All @@ -27,6 +29,8 @@ class Function(object):
args = None
#: shows whether the function requires a context to run.
needs_context = False
#: shows which protocols may use the function
protocols = None

def __call__(self, context, **args):
"""
Expand All @@ -42,6 +46,10 @@ def __call__(self, context, **args):
return self.call(**args)

@cached_property
def name(self):
return self.route.name

@property
def type_name(self):
return self.name

Expand Down Expand Up @@ -70,7 +78,7 @@ class NativeFunctionAdapter(Function):

def __init__(
self, origin,
name=None, return_type=str, args=None, needs_context=False):
route=None, return_type=str, args=None, needs_context=False, protocols=None):
"""
``origin`` is a native Python function to be wrapped.
``name`` is the name of the function, if it is not specified,
Expand All @@ -96,7 +104,7 @@ def __init__(
>>> a = NativeFunctionAdapter(nothing)
>>> a.origin == nothing
True
>>> a.name
>>> a.route.name
'nothing'
>>> a.return_type
<class 'pyws.functions.args.types.simple.String'>
Expand All @@ -110,7 +118,7 @@ def __init__(
... c = a + b
... return c
>>> a = NativeFunctionAdapter(add, name='concat')
>>> a = NativeFunctionAdapter(add, route='concat')
>>> a.name
'concat'
>>> len(a.args.fields)
Expand All @@ -127,7 +135,7 @@ def __init__(
>>> a = NativeFunctionAdapter(
... add,
... name='sum',
... route='sum',
... return_type=int,
... args=(int,),
... )
Expand All @@ -149,14 +157,24 @@ def __init__(

self.origin = origin

self.name = name or origin.__name__
if not route:
self.route = NameRoute(origin.__name__)
elif isinstance(route, basestring):
self.route = NameRoute(route)
elif isinstance(route, Route):
self.route = route
else:
raise ConfigurationError(
'route must be either a string or a Route instance')
self.documentation = origin.__doc__
self.return_type = TypeFactory(return_type or str)
self.needs_context = needs_context
self.protocols = protocols

# Get argument names from origin
arg_names = [(x, ) for x in getargspec(origin)[0]
if not needs_context or x != CONTEXT_ARG_NAME]
arg_names = [
(x, ) for x in getargspec(origin)[0]
if not needs_context or x != CONTEXT_ARG_NAME]

# Get argument types
if not args:
Expand Down
39 changes: 21 additions & 18 deletions src/pyws/functions/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@

class FunctionManager(object):

def get_one(self, context, name):
def get_one(self, context, route, protocol):
"""
Returns a function by its name if it is accessible in the context. If
it is not accessible or does not exist, raises
Returns a function by its route if it is accessible in the ``context``
for the ``protocol``. If it is not accessible or does not exist, raises
``pyws.errors.FunctionNotFound``. Read more about context in chaper
:doc:`context` and about functions in chapter :doc:`function`.
"""
raise NotImplementedError('FunctionManager.get_one')

def get_all(self, context):
def get_all(self, context, protocol):
"""
Returns a list of functions accessible in the context. Read more about
context in chaper :doc:`context` and about functiona in chapter
:doc:`function`.
Returns a list of functions accessible in the ``context`` for
the ``protocol``. Read more about context in chaper :doc:`context` and
about functiona in chapter :doc:`function`.
"""
raise NotImplementedError('FunctionManager.get_all')

Expand All @@ -32,7 +32,7 @@ def __init__(self, *functions):
"""
``functions`` is a list of functions to be registered.
"""
self.functions = {}
self.functions = []
for function in functions:
self.add_function(function)

Expand All @@ -45,22 +45,25 @@ def add_function(self, function):
"""
Adds the function to the list of registered functions.
"""
function = self.build_function(function)
if function.name in self.functions:
if filter(lambda other: function.route == other.route, self.functions):
raise FunctionAlreadyRegistered(function.name)
self.functions[function.name] = function
self.functions.append(self.build_function(function))

def get_one(self, context, name):
def _get_by_protocol(self, protocol):
return (f for f in self.functions if f.route.check_protocol(protocol))

def get_one(self, context, route, protocol):
"""
Returns a function if it is registered, the context is ignored.
"""
try:
return self.functions[name]
except KeyError:
raise FunctionNotFound(name)
for function in self._get_by_protocol(protocol):
match, args = function.route.match(route)
if match:
return function, args
raise FunctionNotFound(route)

def get_all(self, context):
def get_all(self, context, protocol):
"""
Returns a list of registered functions, the context is ignored.
"""
return self.functions.values()
return list(self._get_by_protocol(protocol))
13 changes: 8 additions & 5 deletions src/pyws/functions/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from pyws.functions import NativeFunctionAdapter
from pyws.server import SERVERS

def register(*args, **kwargs):

def register(route=None, *args, **kwargs):
"""
Creates a registrator that, being called with a function as the only
argument, wraps a function with ``pyws.functions.NativeFunctionAdapter``
Expand All @@ -21,14 +22,14 @@ def register(*args, **kwargs):
>>> @register()
... def say_hello(name):
... return 'Hello, %s' % name
>>> server.get_functions(context=None)[0].name
>>> server.get_functions(context=None, protocol=None)[0].name
'say_hello'
>>> another_server = Server(dict(NAME='another_server'))
>>> @register(to='another_server', return_type=int, args=(int, 0))
... def gimme_more(x):
... return x * 2
>>> another_server.get_functions(context=None)[0].name
>>> another_server.get_functions(context=None, protocol=None)[0].name
'gimme_more'
"""
Expand All @@ -41,7 +42,9 @@ def registrator(origin):
server = SERVERS[kwargs.pop('to')]
except KeyError:
server = SERVERS.default
server.add_function(
NativeFunctionAdapter(origin, *args, **kwargs))
routes = route if isinstance(route, (list, tuple)) else [route]
for r in routes:
server.add_function(
NativeFunctionAdapter(origin, r, *args, **kwargs))
return origin
return registrator
43 changes: 42 additions & 1 deletion src/pyws/protocols/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
from pyws.errors import ET_CLIENT

__all__ = ('Protocol', )
__all__ = ('Protocol', 'Route', 'NameRoute')


class Route(object):

protocols = None

@property
def name(self):
raise NotImplementedError('Route.name')

def __eq__(self, other):
raise NotImplementedError('Route.__eq__')

def match(self, other):
raise NotImplementedError('Route.match')

def __ne__(self, other):
return not self.__eq__(other)

def check_protocol(self, protocol):
return self.protocols is None or \
protocol in self.protocols or type(protocol) in self.protocols


class NameRoute(Route):

def __init__(self, name):
self._name = name

def __str__(self):
return '<NameRoute(%s)>' % self.name

@property
def name(self):
return self._name

def __eq__(self, other):
return type(self) == type(other) and self.name == other.name

def match(self, other):
return (self == other or self.name == other), {}


class Protocol(object):
Expand Down
Loading

0 comments on commit 0942a48

Please sign in to comment.