diff --git a/examples/calculator_service/.dockerignore b/examples/calculator_service/.dockerignore new file mode 100644 index 0000000..d7a2f35 --- /dev/null +++ b/examples/calculator_service/.dockerignore @@ -0,0 +1,2 @@ +Dockerfile +*.pyc diff --git a/examples/calculator_service/Dockerfile b/examples/calculator_service/Dockerfile new file mode 100644 index 0000000..423edac --- /dev/null +++ b/examples/calculator_service/Dockerfile @@ -0,0 +1,15 @@ +FROM python:2.7 + +ENV PYTHONUNBUFFERED 1 + +RUN mkdir /code +WORKDIR /code + +RUN pip install uwsgi + +# ADD requirements.txt /code/ +# RUN pip install -r requirements.txt +RUN pip install -e git+https://github.com/brianz/servant.git#egg=servant + +COPY . /code/ +RUN pip install -e . diff --git a/examples/calculator_service/README.md b/examples/calculator_service/README.md new file mode 100644 index 0000000..4c1890a --- /dev/null +++ b/examples/calculator_service/README.md @@ -0,0 +1,28 @@ +# Calculator Service demo + +This is a really simple/dummy service which implements a calculator with Servant. There is a +Docker file which can make it easier to get up and running. To run this: + +``` +$ ./build.sh +$ docker run -it --rm servant/calculator bash +root@557b6ba4e2c9:/code# python test_calculator.py +100.0 / 6.0 = 16.6666666667 +``` + +It's also possible to mount the current working directory as a volume so that you can hack on the +`test_calculator.py` script + +``` +docker run -it --rm -v `pwd`:/code servant/calculator bash +``` + +Now you can edit any files on your host system and that is reflected in the Docker container automatically. + +In this default configuration, service calls are executed as a local Python library call. In order to execute the service calls over HTTP, stand up the uwsgi http server: + +``` +docker run -it --rm -v `pwd`:/code -p 8888:8888 servant/calculator uwsgi --ini uwsgi.ini +``` + +Now, from your host system, uncomment the `client.configure()` line in `test_calculator.py`, update the host and port (which is 8888 in our example above) and launch `test_calculator.py` from your host system. You should get the exact same response and see uwsgi reply to the request. diff --git a/examples/calculator_service/build.sh b/examples/calculator_service/build.sh new file mode 100755 index 0000000..ee917d4 --- /dev/null +++ b/examples/calculator_service/build.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +docker build -t servant/calculator . diff --git a/examples/calculator_service/calculator_service/__init__.py b/examples/calculator_service/calculator_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/calculator_service/calculator_service/actions.py b/examples/calculator_service/calculator_service/actions.py new file mode 100644 index 0000000..2e9edbf --- /dev/null +++ b/examples/calculator_service/calculator_service/actions.py @@ -0,0 +1,55 @@ +import servant.fields + +from servant.exceptions import ActionError +from servant.exceptions import ServantException +from servant.service.actions import Action + + +class AddAction(Action): + number1 = servant.fields.IntField( + required=True, + ) + number2 = servant.fields.IntField( + required=True, + ) + result = servant.fields.IntField( + in_response=True, + ) + + def run(self, **kwargs): + self.result = self.number1 + self.number2 + + +class SubtractAction(AddAction): + def run(self, **kwargs): + self.result = self.number1 - self.number2 + + +class BackwardSubtractAction(SubtractAction): + def run(self, **kwargs): + self.result = self.number2 - self.number1 + + +class MultiplyAction(AddAction): + def run(self, **kwargs): + self.result = self.number1 * self.number2 + + +class DivideAction(Action): + numerator = servant.fields.DecimalField( + required=True, + in_response=True, + ) + denominator = servant.fields.DecimalField( + required=True, + in_response=True, + ) + quotient = servant.fields.DecimalField( + in_response=True, + ) + + def run(self, **kwargs): + if self.denominator == 0: + raise ActionError('Cannot divide by zero') + self.quotient = self.numerator / self.denominator + diff --git a/examples/calculator_service/calculator_service/config.py b/examples/calculator_service/calculator_service/config.py new file mode 100644 index 0000000..ac87d6a --- /dev/null +++ b/examples/calculator_service/calculator_service/config.py @@ -0,0 +1,3 @@ +DUMMY_CONFIG = True +VERSION = 1.0 +CALCULATOR_NAME = 'calculator_service' diff --git a/examples/calculator_service/calculator_service/service.py b/examples/calculator_service/calculator_service/service.py new file mode 100644 index 0000000..fcb848d --- /dev/null +++ b/examples/calculator_service/calculator_service/service.py @@ -0,0 +1,23 @@ +from servant.service.base import Service + +import actions + + +class CalculatorService(Service): + + name = 'calculator_service' + version = 1 + + action_map = { + 'add': actions.AddAction, + 'subtract': actions.SubtractAction, + 'divide': actions.DivideAction, + } + + +class CalculatorServiceV2(CalculatorService): + version = 2 + action_map = { + 'multiply': actions.MultiplyAction, + 'subtract': actions.BackwardSubtractAction, + } diff --git a/examples/calculator_service/requirements.txt b/examples/calculator_service/requirements.txt new file mode 100644 index 0000000..24a11d3 --- /dev/null +++ b/examples/calculator_service/requirements.txt @@ -0,0 +1 @@ +git+https://github.com/brianz/servant.git diff --git a/examples/calculator_service/setup.py b/examples/calculator_service/setup.py new file mode 100644 index 0000000..d6fb86e --- /dev/null +++ b/examples/calculator_service/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages + +setup( + name = 'calculator_service', + packages=find_packages(), + version = '1.0', + description = 'Example application with Servant', + author='Brian Zambrano', + author_email='brianz@gmail.com', + classifiers=[ + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: OS Independent', + 'Development Status :: 1 - Planning', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Topic :: System :: Distributed Computing', + ] +) diff --git a/examples/calculator_service/test_calculator.py b/examples/calculator_service/test_calculator.py new file mode 100644 index 0000000..81c0cc2 --- /dev/null +++ b/examples/calculator_service/test_calculator.py @@ -0,0 +1,68 @@ +import servant.client + +calc_client = servant.client.Client('calculator_service', version=1) +simple_client = servant.client.Client('simple_service', version=1) +calc_client2 = servant.client.Client('calculator_service', version=2) + +# Uncomment this line and change the connection settings in order to hit an HTTP version of the +# service +# client.configure('http', host='192.168.88.100', port=8888) + +def _handle_error(response): + print response.errors, response.field_errors + + +def do_add(client): + response = client.add(number1=10, number2=15) + + if response.is_error(): + _handle_error(response) + else: + print response.result + + +def do_subtract(client): + response = client.subtract(number1=10, number2=15) + + if response.is_error(): + _handle_error(response) + else: + print response.result + + +def do_divide(client): + response = client.divide(numerator=100, denominator=6) + # Here are some examples of requests which throw errors + #response = client.divide(numerator=100, denominator=0) + #response = client.divide(numerator=100, denominator='abc') + + if response.is_error(): + _handle_error(response) + else: + print '%s / %s = %s' % (response.numerator, response.denominator, response.quotient) + +def do_multiply(client): + response = client.multiply(number1=12, number2=12) + + if response.is_error(): + print response.errors + else: + print response.result + + +calc_client.configure('http', host='192.168.88.100', port=8888) +calc_client2.configure('http', host='192.168.88.100', port=8888) + +do_divide(calc_client) + +#response = simple_client.get_theater_listing(theater_id=123) +#if not response.is_error(): +# for movie in response.movies: +# print movie +#else: +# print response.text +# + +do_add(calc_client2) +#calc_client2.service_name = 'fooely' +#do_multiply(calc_client2) diff --git a/examples/calculator_service/uwsgi.ini b/examples/calculator_service/uwsgi.ini new file mode 100644 index 0000000..1e07451 --- /dev/null +++ b/examples/calculator_service/uwsgi.ini @@ -0,0 +1,6 @@ +[uwsgi] +http=0.0.0.0:8888 +wsgi-file=wsgi_handler.py +processes=1 +threads=1 +py-auto-reload=3 diff --git a/examples/calculator_service/wsgi_handler.py b/examples/calculator_service/wsgi_handler.py new file mode 100644 index 0000000..1640041 --- /dev/null +++ b/examples/calculator_service/wsgi_handler.py @@ -0,0 +1,4 @@ +from calculator_service.service import CalculatorService + +service = CalculatorService() +application = service.get_wsgi_application diff --git a/examples/simple_service/client.py b/examples/simple_service/client.py new file mode 100644 index 0000000..c030c14 --- /dev/null +++ b/examples/simple_service/client.py @@ -0,0 +1,50 @@ +import sys +import time + +import servant.client + +from pprint import pprint as pp + +client = servant.client.Client('simple_service', version=1) + +try: + if sys.argv[1].lower() == 'http': + client.configure('http', host='localhost', port=8888) +except Exception: + pass + +if not client.is_configured(): + client.configure('local') + +def line(): + print + print '-' * 50 + +#resp = client.ping() +#line() +#pp(resp) + +#resp = client.echo(name='brianz') +#line() +#pp(resp) + +#resp = client.echo(name='brianz', age=41, is_awesome=True) +#line() +#pp(resp) + +#resp = client.get_weather(city='denver', state='CO') +#line() +#pp(resp) +# +n = time.time() - (3600 * 4) + +#resp = client.get_historical_weather( +# city_id=2885679, +# start_time=n, +# make_real_request=False) +#line() +#pp(resp) + +resp = client.get_theater_listing(theater_id=1234) +line() +pp(resp) diff --git a/examples/simple_service/setup.py b/examples/simple_service/setup.py new file mode 100644 index 0000000..899e9f4 --- /dev/null +++ b/examples/simple_service/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages + +setup( + name = 'simple_service', + packages=find_packages(), + version = '1.0', + description = 'Example application with Servant', + author='Brian Zambrano', + author_email='brianz@gmail.com', + classifiers=[ + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Operating System :: OS Independent', + 'Development Status :: 1 - Planning', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Topic :: System :: Distributed Computing', + ] +) diff --git a/examples/simple_service/simple_service/__init__.py b/examples/simple_service/simple_service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/simple_service/simple_service/service.py b/examples/simple_service/simple_service/service.py new file mode 100644 index 0000000..25f4f0a --- /dev/null +++ b/examples/simple_service/simple_service/service.py @@ -0,0 +1,178 @@ +import time +import requests + +from datetime import datetime + +import servant.fields + +from servant.service.base import Service +from servant.service.actions import ( + Action, + stdactions, +) + +from schematics.exceptions import ValidationError + +def now(): + return time.time() + + +class WeatherSearchAction(Action): + + WEATHER_URL = 'http://api.openweathermap.org/data/2.5/weather' + city = servant.fields.StringField( + max_length=64, + ) + + def run(self, **kwargs): + response = requests.get( + WeatherSearchAction.WEATHER_URL, + params={'q': '%s,us' % self.city} + ) + return response.json() + + +_fake_response = {u'calctime': 0.8057, + u'city_id': 2885679, + u'cnt': 2, + u'cod': u'200', + u'list': [{u'clouds': {u'all': 90}, + u'dt': 1416331494, + u'main': {u'humidity': 75, + u'pressure': 1009, + u'temp': 279.92, + u'temp_max': 281.15, + u'temp_min': 278.85}, + u'weather': [{u'description': u'light rain', + u'icon': u'10n', + u'id': 500, + u'main': u'Rain'}], + u'wind': {u'deg': 0, u'speed': 1}}, + {u'clouds': {u'all': 75}, + u'dt': 1416340783, + u'main': {u'humidity': 93, + u'pressure': 1011, + u'temp': 279.63, + u'temp_max': 282.15, + u'temp_min': 278.15}, + u'weather': [{u'description': u'broken clouds', + u'icon': u'04n', + u'id': 803, + u'main': u'Clouds'}], + u'wind': {u'deg': 180.009, u'speed': 1.11}}], + u'message': u''} + + +class HistoricalWeatherSearchAction(Action): + + WEATHER_URL = 'http://api.openweathermap.org/data/2.5/history/city' + + # input only fields + city_id = servant.fields.IntField( + required=True, + ) + make_real_request = servant.fields.BooleanField( + default=False, + ) + + # input and output fields + start_time = servant.fields.IntField( + required=True, + in_response=True, + ) + end_time = servant.fields.IntField( + default=now, + in_response=True, + ) + + # output only fields + city_name = servant.fields.StringField( + in_response=True, + max_length=40, + ) + #calctime = servant.fields.DecimalField( + calctime = servant.fields.FloatField( + serialized_name='response_time', + in_response=True, + ) + + def validate_start_time(self, data, value): + if data['start_time'] >= data['end_time']: + raise ValidationError(u'start_time must be before end_time') + return value + + def run(self, **kwargs): + """Main entry point to entire service.""" + if self.make_real_request: + resp = self._make_request() + else: + resp = _fake_response + self.city_name = self.get_city_name_by_id(self.city_id) + self.calctime = resp['calctime'] + + def get_city_name_by_id(self, city_id): + db = {2885679: 'Denver'} + return db.get(city_id, 'Unknown City') + + def _make_request(self): + response = requests.get( + HistoricalWeatherSearchAction.WEATHER_URL, + params={ + 'id': self.city_id, 'type': 'hour', + 'start': self.start_time, 'end': self.end_time + } + ) + return response.json() + + +class MovieField(servant.fields.ContainerField): + name = servant.fields.StringField() + director = servant.fields.StringField() + release_date = servant.fields.DateTimeField() + + +class TheaterListingAction(Action): + + theater_id = servant.fields.IntField( + required=True, + ) + theater_name = servant.fields.StringField( + in_response=True, + max_length=48, + ) + movies = servant.fields.ListField( + MovieField, + in_response=True, + ) + + def get_movies_from_db(self, theater_id): + return [ + MovieField( + {'name': 'Intersteller', + 'director': 'Christopher Nolan', + 'release_date': datetime(2014, 11, 1)} + ), + MovieField( + {'name': 'Dumb and Dumber To', + 'director': 'Peter Farrelly, Bobby Farrelly', + 'release_date': datetime(2014, 11, 5)}, + ), + ] + + def run(self, **kwargs): + self.movies = self.get_movies_from_db(self.theater_id) + self.theater_name = u'Cinemark Fort Collins 16' + + +class SimpleService(Service): + + name = 'simple_service' + version = 1 + + action_map = { + 'echo': stdactions.EchoAction, + 'ping': stdactions.PingAction, + 'get_weather': WeatherSearchAction, + 'get_historical_weather': HistoricalWeatherSearchAction, + 'get_theater_listing': TheaterListingAction, + } diff --git a/examples/simple_service/uwsgi.ini b/examples/simple_service/uwsgi.ini new file mode 100644 index 0000000..a039d3d --- /dev/null +++ b/examples/simple_service/uwsgi.ini @@ -0,0 +1,8 @@ +[uwsgi] +http=127.0.0.1:8888 +wsgi-file=wsgi_handler.py +processes=1 +threads=1 +venv=/Users/brianz/.virtualenvs/element +pythonpath=/Users/brianz/work/servant/ +py-auto-reload=3 diff --git a/examples/simple_service/wsgi_handler.py b/examples/simple_service/wsgi_handler.py new file mode 100644 index 0000000..d78e328 --- /dev/null +++ b/examples/simple_service/wsgi_handler.py @@ -0,0 +1,4 @@ +from service import SimpleService + +server = SimpleService() +application = server.get_wsgi_application diff --git a/examples/simple_service/wsgi_server.py b/examples/simple_service/wsgi_server.py new file mode 100644 index 0000000..193e7d5 --- /dev/null +++ b/examples/simple_service/wsgi_server.py @@ -0,0 +1,10 @@ +from wsgiref.simple_server import make_server +from wsgiref.validate import validator + +from wsgi_handler import application + +validator_app = validator(application) + +httpd = make_server('', 8888, validator_app) +print "Serving on port 8888..." +httpd.serve_forever()