Skip to content

Commit d68fca6

Browse files
committed
Merge pull request #33 from BenjamenMeyer/enhancement_subservices
Enhancement: Enable Sub-Services for better API end-point management
2 parents 24b65dc + 73f70f9 commit d68fca6

File tree

3 files changed

+212
-40
lines changed

3 files changed

+212
-40
lines changed

stackinabox/services/service.py

+141-37
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,93 @@ class InvalidRouteRegexError(StackInABoxServiceErrors):
2323
pass
2424

2525

26+
class StackInABoxServiceRouter(object):
27+
28+
def __init__(self, name, uri, obj, parent_obj):
29+
self.service_name = name
30+
self.uri = uri
31+
self.obj = obj
32+
self.parent_obj = parent_obj
33+
self.methods = {}
34+
35+
# Ensure we do not have any circular references
36+
assert(self.obj != self.parent_obj)
37+
38+
@property
39+
def is_subservice(self):
40+
if self.obj is not None:
41+
return True
42+
43+
return False
44+
45+
def set_subservice(self, obj):
46+
if self.obj is not None:
47+
raise RouteAlreadyRegisteredError(
48+
'Service Router ({0} - {1}): Route {2} already has a '
49+
'sub-service handler'
50+
.format(id(self), self.service_name, self.uri))
51+
52+
if len(self.methods):
53+
logger.debug(
54+
'WARNING: Service Router ({0} - {1}): Methods detected '
55+
'on Route {2}. Sub-Service {3} may be hidden.'
56+
.format(id(self), self.service_name, self.uri, obj.name))
57+
58+
# Ensure we do not have any circular references
59+
assert(obj != self.parent_obj)
60+
self.obj = obj
61+
self.obj.base_url = '{0}/{1}'.format(self.uri, self.service_name)
62+
63+
def update_uris(self, new_uri):
64+
self.uri = new_uri
65+
if self.obj:
66+
self.obj.base_url = '{0}/{1}'.format(self.uri, self.service_name)
67+
68+
def register_method(self, method, fn):
69+
if method not in self.methods.keys():
70+
logger.debug('Service Router ({0} - {1}): Adding method {2} on '
71+
'route {3}'
72+
.format(id(self),
73+
self.service_name,
74+
method,
75+
self.uri))
76+
self.methods[method] = fn
77+
78+
else:
79+
raise RouteAlreadyRegisteredError(
80+
'Service Router ({0} - {1}): Method {2} already registered '
81+
'on Route {3}'
82+
.format(id(self),
83+
self.service_name,
84+
method,
85+
self.uri))
86+
87+
def __call__(self, method, request, uri, headers):
88+
if method in self.methods:
89+
logger.debug('Service Router ({0} - {1}): Located Method {2} on '
90+
'Route {3}. Calling...'
91+
.format(id(self),
92+
self.service_name,
93+
method,
94+
self.uri))
95+
return self.methods[method](self.parent_obj,
96+
request,
97+
uri,
98+
headers)
99+
else:
100+
logger.debug('Service Router ({0} - {1}): Located Subservice {2} '
101+
'on Route {3}. Calling...'
102+
.format(id(self),
103+
self.service_name,
104+
self.obj.name,
105+
self.uri))
106+
107+
return self.obj.sub_request(method,
108+
request,
109+
uri,
110+
headers)
111+
112+
26113
class StackInABoxService(object):
27114
DELETE = 'DELETE'
28115
GET = 'GET'
@@ -51,33 +138,41 @@ def __init__(self, name):
51138
.format(self.__id, self.name))
52139

53140
@staticmethod
54-
def __is_regex(uri):
141+
def is_regex(uri):
55142
regex_type = type(re.compile(''))
56143
return isinstance(uri, regex_type)
57144

58145
@staticmethod
59-
def validate_regex(regex):
146+
def validate_regex(regex, sub_service):
60147
# The regex generated by stackinabox starts with ^
61148
# and ends with $. Enforce that the provided regex does the same.
62149

63150
if regex.pattern.startswith('^') is False:
64151
logger.debug('StackInABoxService: Pattern must start with ^')
65152
raise InvalidRouteRegexError('Pattern must start with ^')
66153

67-
if regex.pattern.endswith('$') is False:
154+
# Note: pattern may end with $ even if sub_service is True
155+
if regex.pattern.endswith('$') is False and sub_service is False:
68156
logger.debug('StackInABoxService: Pattern must end with $')
69157
raise InvalidRouteRegexError('Pattern must end with $')
70158

159+
# Enforce that if the pattern does not end with $ that it is a service
160+
if regex.pattern.endswith('$') is True and sub_service is True:
161+
logger.debug(
162+
'StackInABoxService: Sub-Service RegEx Pattern must not '
163+
'end with $')
164+
raise InvalidRouteRegexError('Pattern must end with $')
165+
71166
@staticmethod
72-
def __get_service_regex(base_url, service_url):
167+
def get_service_regex(base_url, service_url, sub_service):
73168
# if the specified service_url is already a regex
74169
# then just use. Otherwise create what we need
75-
if StackInABoxService.__is_regex(service_url):
170+
if StackInABoxService.is_regex(service_url):
76171
logger.debug('StackInABoxService: Received regex {0} for use...'
77172
.format(service_url.pattern))
78173

79174
# Validate the regex against StackInABoxService requirement
80-
StackInABoxService.validate_regex(service_url)
175+
StackInABoxService.validate_regex(service_url, sub_service)
81176

82177
return service_url
83178
else:
@@ -100,8 +195,10 @@ def base_url(self, value):
100195
value))
101196
self.__base_url = value
102197
for k, v in six.iteritems(self.routes):
103-
v['regex'] = StackInABoxService.__get_service_regex(value,
104-
v['uri'])
198+
v['regex'] = StackInABoxService.get_service_regex(
199+
value,
200+
v['uri'],
201+
v['handlers'].is_subservice)
105202

106203
def reset(self):
107204
logger.debug('StackInABoxService ({0}): Reset'
@@ -110,10 +207,8 @@ def reset(self):
110207
logger.debug('StackInABoxService ({0}): Hosting Service {1}'
111208
.format(self.__id, self.name))
112209

113-
def request(self, method, request, uri, headers):
114-
logger.debug('StackInABoxService ({0}:{1}): Received {2} - {3}'
115-
.format(self.__id, self.name, method, uri))
116-
uri_path = uri
210+
def try_handle_route(self, route_uri, method, request, uri, headers):
211+
uri_path = route_uri
117212
if '?' in uri:
118213
logger.debug('StackInABoxService ({0}:{1}): Found query string '
119214
'removing for match operation.'
@@ -137,38 +232,47 @@ def request(self, method, request, uri, headers):
137232
logger.debug('StackInABoxService ({0}:{1}): Checking if '
138233
'route {2} handles method {2}...'
139234
.format(self.__id, self.name, v['uri'], method))
140-
if method in v['handlers']:
141-
logger.debug('StackInABoxService ({0}:{1}): Calling '
142-
'handler for route {2} on method {3}...'
143-
.format(self.__id,
144-
self.name,
145-
v['uri'],
146-
method))
147-
return v['handlers'][method](self,
148-
request,
149-
uri,
150-
headers)
235+
return v['handlers'](method,
236+
request,
237+
uri,
238+
headers)
151239
return (500, headers, 'Server Error')
152240

153-
def register(self, method, uri, call_back):
154-
found = False
241+
def request(self, method, request, uri, headers):
242+
logger.debug('StackInABoxService ({0}:{1}): Request Received {2} - {3}'
243+
.format(self.__id, self.name, method, uri))
244+
return self.try_handle_route(uri, method, request, uri, headers)
245+
246+
def sub_request(self, method, request, uri, headers):
247+
logger.debug('StackInABoxService ({0}:{1}): Sub-Request Received '
248+
'{2} - {3}'
249+
.format(self.__id, self.name, method, uri))
250+
return self.request(method, request, uri, headers)
155251

252+
def create_route(self, uri, sub_service):
156253
if uri not in self.routes.keys():
157254
logger.debug('Service ({0}): Creating routes'
158255
.format(self.name))
159256
self.routes[uri] = {
160-
'regex': StackInABoxService.__get_service_regex(self.base_url,
161-
uri),
257+
'regex': StackInABoxService.get_service_regex(self.base_url,
258+
uri,
259+
sub_service),
162260
'uri': uri,
163-
'handlers': {
164-
}
261+
'handlers': StackInABoxServiceRouter(self.name,
262+
uri,
263+
None,
264+
self)
165265
}
166266

167-
if method not in self.routes[uri]['handlers'].keys():
168-
logger.debug('Service ({0}): Adding route for {1}'
169-
.format(self.name, method))
170-
self.routes[uri]['handlers'][method] = call_back
171-
else:
172-
raise RouteAlreadyRegisteredError(
173-
'Service ({0}): Route {1} already registered'
174-
.format(self.name, uri))
267+
def register(self, method, uri, call_back):
268+
found = False
269+
270+
self.create_route(uri, False)
271+
self.routes[uri]['handlers'].register_method(method,
272+
call_back)
273+
274+
def register_subservice(self, uri, service):
275+
found = False
276+
277+
self.create_route(uri, True)
278+
self.routes[uri]['handlers'].set_subservice(service)

stackinabox/tests/test_service.py

+70-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import re
22
import unittest
33

4+
import httpretty
5+
import requests
46

7+
from stackinabox.stack import StackInABox
58
from stackinabox.services.service import *
9+
import stackinabox.util_httpretty
610

711

812
class TestServiceRegex(unittest.TestCase):
@@ -13,7 +17,18 @@ def setUp(self):
1317
def tearDown(self):
1418
super(TestServiceRegex, self).tearDown()
1519

16-
def test_stackinabox_service_regex(self):
20+
def test_instantiation(self):
21+
name = 'test-service'
22+
23+
service = StackInABoxService(name)
24+
self.assertEqual(service.name, name)
25+
self.assertTrue(service.base_url.startswith('/'))
26+
self.assertTrue(service.base_url.endswith(name))
27+
self.assertEqual(len(service.base_url),
28+
(len(name) + 1))
29+
self.assertEqual(len(service.routes), 0)
30+
31+
def test_stackinabox_validate_regex(self):
1732

1833
positive_cases = [
1934
re.compile('^/$')
@@ -25,11 +40,34 @@ def test_stackinabox_service_regex(self):
2540
]
2641

2742
for case in positive_cases:
28-
StackInABoxService.validate_regex(case)
43+
StackInABoxService.validate_regex(case, False)
2944

3045
for case in negative_cases:
3146
with self.assertRaises(InvalidRouteRegexError):
32-
StackInABoxService.validate_regex(case)
47+
StackInABoxService.validate_regex(case, False)
48+
49+
def test_stackinabox_service_regex(self):
50+
51+
case_regex = [
52+
re.compile('^/$')
53+
]
54+
55+
case_nonregex = [
56+
'/',
57+
]
58+
59+
for case in case_regex:
60+
self.assertEqual(StackInABoxService.get_service_regex('base',
61+
case,
62+
False),
63+
case)
64+
65+
for case in case_nonregex:
66+
case_regex = re.compile('^{0}$'.format(case))
67+
self.assertEqual(StackInABoxService.get_service_regex('base',
68+
case,
69+
False),
70+
case_regex)
3371

3472

3573
class AnotherAdvancedService(StackInABoxService):
@@ -46,13 +84,25 @@ def second_handler(self, request, uri, headers):
4684
return (200, headers, 'howdy')
4785

4886

87+
class YetAnotherService(StackInABoxService):
88+
89+
def __init__(self):
90+
super(YetAnotherService, self).__init__('yaas')
91+
self.register(StackInABoxService.GET, '^/french$',
92+
YetAnotherService.yaas_handler)
93+
94+
def yaas_handler(self, request, uri, headers):
95+
return (200, headers, 'bonjour')
96+
97+
4998
class TestServiceRouteRegistration(unittest.TestCase):
5099

51100
def setUp(self):
52101
super(TestServiceRouteRegistration, self).setUp()
53102

54103
def tearDown(self):
55104
super(TestServiceRouteRegistration, self).tearDown()
105+
StackInABox.reset_services()
56106

57107
def test_bad_registration(self):
58108

@@ -61,3 +111,20 @@ def test_bad_registration(self):
61111
with self.assertRaises(RouteAlreadyRegisteredError):
62112
service.register(StackInABoxService.GET, '/',
63113
AnotherAdvancedService.second_handler)
114+
115+
@httpretty.activate
116+
def test_subservice_registration(self):
117+
service = AnotherAdvancedService()
118+
subservice = YetAnotherService()
119+
service.register_subservice(re.compile('^/french'),
120+
subservice)
121+
122+
StackInABox.register_service(service)
123+
124+
stackinabox.util_httpretty.httpretty_registration('localhost')
125+
126+
res = requests.get('http://localhost/aas/french')
127+
self.assertEqual(res.status_code,
128+
200)
129+
self.assertEqual(res.text,
130+
'bonjour')

tox.ini

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ commands = nosetests {posargs}
1212
[testenv:py34]
1313
deps = -r{toxinidir}/tools/test-requirements.txt
1414
commands = nosetests {posargs}
15+
setenv = VIRTUAL_ENV={envdir} LC_ALL = en_US.utf-8
1516

1617
[testenv:pep8]
1718
deps = setuptools>=1.1.6

0 commit comments

Comments
 (0)