├── __init__.py ├── tests ├── __init__.py └── test_weather_service.py ├── entrypoint.tests.sh ├── entrypoint.sh ├── requirements.txt ├── Dockerfile ├── .circleci └── config.yml ├── README.md ├── api.py ├── .gitignore └── weather_service.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /entrypoint.tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | service rabbitmq-server start 3 | nameko run weather_service & 4 | nose2 -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | service rabbitmq-server start 3 | nameko run weather_service & 4 | python api.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=1.0.2 2 | flask-script>=2.0.6 3 | requests>=2.21.0 4 | nameko>=2.11.0 5 | nose2==0.6.0 6 | nose2[coverage_plugin]>=0.6.5 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | RUN apt-get update -y && \ 4 | apt-get install -y python-pip python-dev build-essential rabbitmq-server 5 | 6 | COPY . /app 7 | 8 | COPY ./entrypoint.sh /app/entrypoint.sh 9 | 10 | WORKDIR /app 11 | 12 | RUN pip install -r requirements.txt 13 | 14 | RUN pip install nose2[coverage_plugin]>=0.6.5 15 | 16 | ENV BASEURL "https://service-homolog.digipix.com.br/v0b" 17 | 18 | RUN [ "chmod", "+x", "/app/entrypoint.sh" ] -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | machine: true 9 | steps: 10 | - checkout 11 | - run: 12 | name: install dependencies 13 | command: | 14 | docker build -t ifood . 15 | - run: 16 | name: run tests 17 | command: | 18 | docker run -it -e X-SPOTIFY-TOKEN=$(echo $X-SPOTIFY-TOKEN) -e X-OPENWM-APPID=504002e265ed827f841600d3259c32ee ifood /app/entrypoint.tests.sh 19 | environment: 20 | X-SPOTIFY-TOKEN: BQBFN7zjOkZZ71QI3W6__3vJR-3cowaf5VAar3mZ2bkWBnnqwzdoT9E9s9NNlb6qvp-6T2ZEdtUOnh9NoyFtkvtKO7VtXnTllkrXongke_fgLQddcXl-IkRsSJYZEhLk2fChVFHGVXa-Vwqx2zo5wGy6hh4a9dEcNCxw3ssNQwu-pmS3l06ogSJoSLM6foVBnq8E41jxRDCHGsHu_2NclCI2tRQpfh_p2lBvWSGc9-DDWEXNX3MWktqzAz-FqBzaPKrsRFoLiRA1ragGQ-5n 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Playlists from Weather [![CircleCI](https://circleci.com/gh/alvaropaco/py-weather-micro-service.svg?style=svg)](https://circleci.com/gh/alvaropaco/py-weather-micro-service) [![Maintainability](https://api.codeclimate.com/v1/badges/3fc099559a53bc7800d0/maintainability)](https://codeclimate.com/github/alvaropaco/py-weather-micro-service/maintainability) 2 | 3 | Tha main goal of this project is retreive a list of Spotify playlist based on current Weather. The Weather is retreived from the Open Weather Map service and filtered by *City* name or *geolocation* coordinates. 4 | 5 | ### Requeriments 6 | 7 | * Docker I/O 8 | 9 | ### Building 10 | 11 | Firstly we need to build the docker image: 12 | 13 | `docker build -t ifood .` 14 | 15 | ### Running 16 | 17 | Run command will push up the micro-service: 18 | 19 | `docker run -it -v $(pwd):/app -p 5000:5000 ifood ./entrypoint.sh` 20 | 21 | ### Usage 22 | 23 | Simple http call to the service URL: 24 | 25 | `curl -X GET 127.0.0.1:5000/playlists?city=new+york -H "X-SPOTIFY-TOKEN: " -H"X-OPENWM-APPID: 504002e265ed827f841600d3259c32ee"` 26 | 27 | ### Testing 28 | 29 | Can run the API tests: 30 | 31 | `docker run -it -v $(pwd):/app -p 5000:5000 ifood ./entrypoint.tests.sh` -------------------------------------------------------------------------------- /api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | from nameko.standalone.rpc import ClusterRpcProxy 3 | 4 | CONFIG = {'AMQP_URI': "amqp://guest:guest@localhost:5672"} 5 | app = Flask(__name__) 6 | 7 | 8 | @app.route('/playlists', methods=['GET']) 9 | def playlists(): 10 | """ 11 | Micro Service Based Weather and Spotify API 12 | This API is made with Flask and Nameko 13 | --- 14 | parameters: 15 | - name: zipcode 16 | in: path 17 | required: true 18 | schema: 19 | type: integer 20 | description: location ZipCode 21 | responses: 22 | 200: 23 | description: Location data 24 | """ 25 | with ClusterRpcProxy(CONFIG) as rpc: 26 | # Get the Spotify Authrization Token from header 27 | # and OWM AppId from the header 28 | openwm_appid = request.headers.get('X-OPENWM-APPID') 29 | spotify_token = request.headers.get('X-SPOTIFY-TOKEN') 30 | 31 | # Consuming Nameko service 32 | # Here we pass the OpenWeatherMap AppId 33 | # and the Spotify JWT 34 | result = rpc.playlists.get_playlists( 35 | openwm_appid, spotify_token, request.args) 36 | return jsonify(result), 200 37 | 38 | 39 | if __name__ == "__main__": 40 | """Start Flask app to serve mircoservices""" 41 | app.run(host='0.0.0.0') 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # VSCode 107 | .vscode -------------------------------------------------------------------------------- /tests/test_weather_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import requests 4 | import json 5 | import os 6 | 7 | from ..api import app 8 | 9 | 10 | class ZipCodeTestCase(unittest.TestCase): 11 | """This class represents the Weather test case""" 12 | 13 | def setUp(self): 14 | """Define test variables and initialize app.""" 15 | 16 | # Getting env variables to Spotify and Open Weather Map 17 | self.spotify_jwt = os.environ.get('X_SPOTIFY_TOKEN') 18 | self.openwm_appid = os.environ.get('X_OPENWM_APPID') 19 | 20 | # Setting parameters to Flask app 21 | app.config['TESTING'] = True 22 | app.config['WTF_CSRF_ENABLED'] = False 23 | app.config['DEBUG'] = False 24 | 25 | # Setting test client 26 | self.app = app.test_client() 27 | self.app.testing = True 28 | 29 | # Check configurations 30 | self.assertEqual(app.debug, False) 31 | 32 | def test_city_name_happy_flow(self): 33 | """Test city name consult""" 34 | 35 | url = 'http://127.0.0.1:5000/playlists?city=sao+paulo' 36 | 37 | res = self.app.get( 38 | url, 39 | headers={ 40 | 'X-SPOTIFY-TOKEN': self.spotify_jwt, 41 | 'X-OPENWM-APPID': self.openwm_appid}) 42 | 43 | jsonObj = json.loads(res.data) 44 | 45 | assert res.status == '200 OK' 46 | assert len(jsonObj) > 0 47 | 48 | def test_lat_lon_happy_flow(self): 49 | """Test search by Lat lon""" 50 | 51 | url = 'http://127.0.0.1:5000/playlists?lat=-23.5506507&lon=-46.6333824' 52 | 53 | res = self.app.get( 54 | url, 55 | headers={ 56 | 'X-SPOTIFY-TOKEN': self.spotify_jwt, 57 | 'X-OPENWM-APPID': self.openwm_appid}) 58 | 59 | jsonObj = json.loads(res.data) 60 | 61 | assert res.status == '200 OK' 62 | assert len(jsonObj) > 0 63 | 64 | def test_without_parameter_happy_flow(self): 65 | """Test to error response""" 66 | 67 | res = self.app.get('http://127.0.0.1:5000/playlists?city=sao+paulo') 68 | 69 | jsonObj = json.loads(res.data) 70 | 71 | assert jsonObj['cod'] == 401 72 | 73 | def tearDown(self): 74 | """teardown all initialized variables.""" 75 | pass 76 | 77 | 78 | # Make the tests conveniently executable 79 | if __name__ == "__main__": 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /weather_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import sys 4 | 5 | from urllib import quote 6 | from nameko.rpc import rpc, RpcProxy 7 | import requests 8 | 9 | openweather_base_url = 'http://api.openweathermap.org/data/2.5/weather' 10 | spotify_base_url = 'https://api.spotify.com/v1/' 11 | 12 | 13 | def error(msg): 14 | """Return a Exception object 15 | 16 | This function is a reuse function to handle exceptions 17 | """ 18 | raise Exception(msg) 19 | 20 | 21 | def request(qstring, jwt): 22 | """ 23 | Return the result from a Request 24 | """ 25 | bearer = 'Bearer {}'.format(jwt) 26 | 27 | # JWT necessary to the Spotify Authorization 28 | if jwt is None: 29 | return requests.request('GET', qstring) 30 | else: 31 | # If is not a Spotify call 32 | return requests.request( 33 | 'GET', qstring, headers={ 34 | 'Authorization': bearer}) 35 | 36 | 37 | def get_playlists(weather, jwt): 38 | """ Return a list of Spotify playlists 39 | 40 | From a Weather predict, we consult the 41 | Spotify API to return results based on the 42 | weather description. 43 | """ 44 | 45 | filtered = [] 46 | 47 | if weather is None: 48 | error('Missing parameter') 49 | 50 | if weather['weather'] is None: 51 | error('Invalid location') 52 | 53 | # Main attributte contains the temperature meassure 54 | main = weather['main'] 55 | 56 | # Convert string to int 57 | temperature = int(main['temp']) 58 | 59 | # Default Genre 60 | genre = 'classical' 61 | 62 | # If temperature (celcius) is above 30 degrees, suggest tracks for party 63 | # In case temperature is between 15 and 30 degrees, suggest pop music tracks 64 | # If it's a bit chilly (between 10 and 14 degrees), suggest rock music tracks 65 | # Otherwise, if it's freezing outside, suggests classical music tracks 66 | if temperature > 30: 67 | genre = 'party' 68 | elif temperature >= 15 and temperature <= 30: 69 | genre = 'pop' 70 | elif temperature >= 10 and temperature <= 14: 71 | genre = 'rock' 72 | 73 | # Get current weather to check temperatury 74 | current = weather['weather'][0] 75 | 76 | qstring = '{}search?q=name:{}&type=playlist'.format( 77 | spotify_base_url, quote(current['description'])) 78 | 79 | resp = request(qstring, jwt).json() 80 | 81 | # Is receives nothing 82 | if 'error' in resp: 83 | e = resp['error'] 84 | error(e['message']) 85 | 86 | # getting plalists itens 87 | items = resp['playlists']['items'] 88 | 89 | # Build the list of the names 90 | for playlist in items: 91 | filtered.append(playlist['name']) 92 | 93 | return filtered 94 | 95 | 96 | def get_weather(args, appid): 97 | """Return a Weather object 98 | 99 | This call consumes the open Weather Map API to retreive the weather information about some geographi coordinate or city name. 100 | """ 101 | 102 | # Check if seach by City 103 | if 'city' not in args: 104 | # Check if search by coordinates 105 | if 'lat' not in args or 'lon' not in args: 106 | error('Missing parameter') 107 | else: 108 | # Query string to coordinates 109 | r_str = '{}?units=metric&lat={}&lon={}&appid={}'.format( 110 | openweather_base_url, args['lat'], args['lon'], appid) 111 | else: 112 | # Query string to City 113 | r_str = '{}?units=metric&q={}&appid={}'.format( 114 | openweather_base_url, quote( 115 | args['city']), appid) 116 | 117 | # Requesting the API 118 | resp = request(r_str, None) 119 | 120 | # Returns a JSON object 121 | return resp.json() 122 | 123 | 124 | class PlaylistsService: 125 | """Playlists Srevice 126 | 127 | Returns: 128 | list: A list of playlists 129 | """ 130 | 131 | name = "playlists" 132 | 133 | zipcode_rpc = RpcProxy('playlistsservice') 134 | 135 | @rpc 136 | def get_playlists(self, appid, jwt, args): 137 | """[summary] 138 | 139 | Arguments: 140 | appid (string): Open Weather Map AppID 141 | jwt (string): Spotify Authorization Token 142 | args (dict): A Dict (city, lat, long) 143 | 144 | Returns: 145 | list: A list of playlists 146 | """ 147 | try: 148 | # Check if is passed args 149 | if args is None: 150 | error("Missing parameter") 151 | 152 | # Consuming the OWM API 153 | weather = get_weather(args, appid) 154 | 155 | # Check response 156 | if int(weather['cod']) != 200: 157 | return weather 158 | 159 | # Consuming Spotify API 160 | playlists = get_playlists(weather, jwt) 161 | 162 | return playlists 163 | except Exception as e: 164 | return str({'Error': str(e)}) 165 | --------------------------------------------------------------------------------