├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── requirements.txt ├── tests ├── conftest.py └── test_api.py └── touristfriend ├── __init__.py ├── api_keys.py ├── business.py ├── external ├── __init__.py ├── google.py └── yelp.py └── service.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | *.pyc 3 | __pycache__/ 4 | 5 | # Unit test / coverage reports 6 | htmlcov/ 7 | .tox/ 8 | .coverage 9 | .coverage.* 10 | .cache 11 | nosetests.xml 12 | coverage.xml 13 | *,cover 14 | .hypothesis/ 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # Mac 35 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | RUN apt-get update -y && \ 4 | apt-get install python3-pip idle3 -y && \ 5 | pip3 install --no-cache-dir --upgrade pip && \ 6 | \ 7 | # delete cache and tmp files 8 | apt-get clean && \ 9 | apt-get autoclean && \ 10 | rm -rf /var/cache/* && \ 11 | rm -rf /tmp/* && \ 12 | rm -rf /var/tmp/* && \ 13 | rm -rf /var/lib/apt/lists/* && \ 14 | \ 15 | # make some useful symlinks that are expected to exist 16 | cd /usr/bin && \ 17 | ln -s idle3 idle && \ 18 | ln -s pydoc3 pydoc && \ 19 | ln -s python3 python && \ 20 | ln -s python3-config python-config && \ 21 | cd / 22 | 23 | # Set the working directory to /touristfriend 24 | WORKDIR ./touristfriend 25 | 26 | # Copy the current directory contents into the container at /touristfriend 27 | ADD . /touristfriend 28 | 29 | # Install any needed packages specified in requirements.txt 30 | RUN pip3 install -r requirements.txt 31 | 32 | # Make port 80 available to the world outside this container 33 | EXPOSE 80 34 | 35 | # Args for private keys 36 | ARG GMAPS_KEY 37 | ARG G_API 38 | ARG YELP_API_KEY 39 | 40 | # Set them for the program 41 | ENV GMAPS_KEY=$GMAPS_KEY 42 | ENV G_API=$G_API 43 | ENV YELP_API_KEY=$YELP_API_KEY 44 | ENV FLASK_APP=touristfriend/__init__.py 45 | 46 | # Ubuntu locale settings 47 | ENV LANG=C.UTF-8 48 | ENV LC_ALL=C.UTF-8 49 | 50 | # Run the app when container launches 51 | CMD ["flask", "run", "--host", "0.0.0.0"] 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2018 Gustavo Rodríguez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TouristFriend 2 | 3 | 4 | TouristFriend is an API for searching and combining results from Google Places and Yelp. 5 | 6 | For the foursquare implementation check out the branch `foursquare` 7 | 8 | Returns a combined list of places ranked by their ratings as a Bayesian estimate 9 | 10 | ### Try it out 11 | ```Bash 12 | $ curl http://touristfriend.club/api/40000/29.743883,-95.361621/restaurants 13 | ``` 14 | 15 | ## Sample output 16 | 17 | ```JavaScript 18 | [ 19 | { 20 | "Rating": "9.49", 21 | "Sources": 1, 22 | "Number_of_Ratings": 868, 23 | "Location": "37.761594,-122.42427", 24 | "Name": "Pizzeria Delfina" 25 | }, 26 | { 27 | "Rating": "9.39", 28 | "Sources": 1, 29 | "Number_of_Ratings": 404, 30 | "Location": "37.7682006597,-122.421604657", 31 | "Name": "Shizen" 32 | }, 33 | { 34 | "Rating": "9.32", 35 | "Sources": 1, 36 | "Number_of_Ratings": 45, 37 | "Location": "37.763093,-122.424281", 38 | "Name": "Turner's Kitchen" 39 | }, 40 | // Up to 15 results... 41 | ] 42 | ``` 43 | 44 | URI Breakdown: `http://touristfriend.club/api/{meters}/{latitude},{longitude}/{query}` 45 | 46 | ## Setup 47 | You'll need to acquire API keys for each of the individual services and add them to api_keys.py. 48 | 49 | + [Google Places](https://developers.google.com/places/web-service/get-api-key) 50 | + [Yelp](https://www.yelp.com/developers/v3/manage_app) 51 | 52 | ### Set environment variables 53 | 54 | ```Bash 55 | # Google Places 56 | $ export G_API=YOUR_GOOGLE_API_KEY 57 | 58 | # Yelp 59 | $ export YELP_API_KEY=YOUR_YELP_API_KEY 60 | 61 | # Flask app, standing on the root of the cloned repo 62 | $ export FLASK_APP=$(pwd)/touristfriend/__init__.py 63 | ``` 64 | ### Install dependencies 65 | 66 | ```Bash 67 | pip install -U Flask flask-cors 68 | ``` 69 | 70 | ### Run it 71 | 72 | ```Bash 73 | # For localhost 74 | $ flask run # 127.0.0.1 75 | # For external server 76 | $ flask run --host "0.0.0.0" & 77 | ``` 78 | 79 | ### Try it 80 | 81 | ```Bash 82 | $ curl http://localhost:5000/api/40000/48.888001,2.337442/restaurants 83 | ``` 84 | 85 | ### With Docker example 86 | 87 | ```Bash 88 | $ docker build -t tfriend --build-arg GMAPS_KEY=YOUR_GOOGLE_MAPS_KEY \ 89 | --build-arg G_API=YOUR_GOOGLE_API_KEY \ 90 | --build-arg YELP_API_KEY=YOUR_YELP_API_KEY . && \ 91 | docker run -p 5000:5000 tfriend 92 | ``` 93 | 94 | Feel free to use the `touristfriend.club` API for testing or demoing, if you plan on using it on production, consider deploying it yourself. 95 | 96 | LICENSE: MIT 97 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask_cors 3 | requests -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from os import path 4 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 5 | from api import app as _app 6 | 7 | 8 | @pytest.fixture 9 | def app(): 10 | return _app 11 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_api 6 | ---------------------------------- 7 | Tests for `api` module. 8 | """ 9 | 10 | 11 | class TestAPI: 12 | def test_api_get(self, client): 13 | res = client.get("/api/40000/48.888001,2.337442/restaurants") 14 | assert res.status_code == 200 15 | -------------------------------------------------------------------------------- /touristfriend/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from flask import Flask 3 | import json 4 | from touristfriend.service import ( 5 | execute_search, combine_duplicate_businesses, write_businesses) 6 | from flask_cors import CORS 7 | 8 | app = Flask(__name__) 9 | CORS(app) 10 | 11 | 12 | @app.route('/api///') 13 | def reviews(distance, location, query): 14 | locations = [] 15 | coords = location.split(",") 16 | try: 17 | location = (float(coords[0]), float(coords[1])) 18 | locations.append(location) 19 | businesses = execute_search(locations, distance, query) 20 | return json.dumps( 21 | write_businesses(combine_duplicate_businesses(businesses))) 22 | except Exception: 23 | return "[]" 24 | -------------------------------------------------------------------------------- /touristfriend/api_keys.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Yelp 4 | Y_KEY = os.environ["YELP_API_KEY"] 5 | 6 | # Google Places 7 | G_API_KEY = os.environ["G_API"] 8 | -------------------------------------------------------------------------------- /touristfriend/business.py: -------------------------------------------------------------------------------- 1 | 2 | class Business: 3 | def __init__(self, name, address, rating, rating_count, location): 4 | self.name = name 5 | self.address = address 6 | self.rating = rating 7 | self.rating_count = rating_count 8 | self.bayesian = 0.0 9 | self.source_count = 1 10 | self.location = location 11 | -------------------------------------------------------------------------------- /touristfriend/external/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/octohedron/TouristFriend/57a307723bdc510f1a2f22ca18e3f851a1914b8d/touristfriend/external/__init__.py -------------------------------------------------------------------------------- /touristfriend/external/google.py: -------------------------------------------------------------------------------- 1 | from touristfriend.api_keys import G_API_KEY 2 | from touristfriend.business import Business 3 | import requests 4 | 5 | SEARCH_URL = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json?location={},{}&radius={}&types={}&key={}' 6 | PLACE_URL = 'https://maps.googleapis.com/maps/api/place/details/json?placeid={}&key={}' 7 | 8 | 9 | def search(lat, lng, distance, query): 10 | """ 11 | Searches the Google Places API (Max Limit = 20) 12 | 13 | :param lat: Latitude of the request 14 | :param long: Longitude of the request 15 | :param distance: Distance to search (meters) 16 | :param query: The niche, i.e. restaurants, bars, etc 17 | :returns: List of retrieved places 18 | """ 19 | 20 | url = SEARCH_URL.format(lat, lng, distance, query, G_API_KEY) 21 | place_list = [] 22 | 23 | data = requests.get(url).json() 24 | 25 | for i in range(0, len(data['results'])): 26 | try: 27 | result = data['results'][i] 28 | place = search_place(result['place_id']) 29 | place_list.append(place) 30 | if len(place_list) == 15: 31 | break 32 | except Exception: 33 | pass 34 | 35 | return place_list 36 | 37 | 38 | def search_place(place_id): 39 | """ 40 | Searches Google for a specific Place 41 | 42 | :param id: Google Place ID 43 | :returns: Business object 44 | """ 45 | url = PLACE_URL.format(place_id, G_API_KEY) 46 | data = requests.get(url).json() 47 | place = data['result'] 48 | try: 49 | if not "hotel" in (place['name'].lower()): 50 | return Business(place['name'], 51 | place['formatted_address'].split(',')[0], 52 | place['rating'], 53 | len(place['reviews']), 54 | (place["geometry"]["location"]["lat"], 55 | place["geometry"]["location"]["lng"])) 56 | except KeyError: 57 | pass 58 | -------------------------------------------------------------------------------- /touristfriend/external/yelp.py: -------------------------------------------------------------------------------- 1 | from touristfriend.api_keys import Y_KEY 2 | from touristfriend.business import Business 3 | import requests 4 | import json 5 | 6 | 7 | def search(lat, lng, distance, query): 8 | """ 9 | Searches the Yelp API (Max Limit = 20) 10 | 11 | :param lat: Latitude of the request 12 | :param long: Longitude of the request 13 | :param distance: Distance to search (meters) 14 | :param query: The niche, i.e. restaurants, bars, etc 15 | :returns: List of retrieved businesses 16 | """ 17 | 18 | headers = {'Authorization': 'Bearer ' + Y_KEY} 19 | 20 | params = {} 21 | params['term'] = query 22 | params['limit'] = 15 23 | params['longitude'] = lng 24 | params['latitude'] = lat 25 | params['radius_filter'] = distance 26 | response = requests.get("https://api.yelp.com/v3/businesses/search", 27 | params=params, headers=headers) 28 | data = json.loads(response.content.decode('utf-8')) 29 | business_list = [] 30 | for i in range(0, len(data['businesses'])): 31 | try: 32 | business = data['businesses'][i] 33 | if business: 34 | business_list.append(Business(business['name'], 35 | business['location'][ 36 | 'display_address'][0], 37 | business['rating'], 38 | business['review_count'], 39 | (business["coordinates"]["latitude"], 40 | business["coordinates"]["longitude"]))) 41 | except IndexError: 42 | pass 43 | 44 | return business_list 45 | -------------------------------------------------------------------------------- /touristfriend/service.py: -------------------------------------------------------------------------------- 1 | from touristfriend.external import google 2 | from touristfriend.external import yelp 3 | import time 4 | 5 | def bayesian(R, v, m, C): 6 | """ 7 | Computes the Bayesian average for the given parameters 8 | 9 | :param R: Average rating for this business 10 | :param v: Number of ratings for this business 11 | :param m: Minimum ratings required 12 | :param C: Mean rating across the entire list 13 | :returns: Bayesian average 14 | """ 15 | 16 | # Convert to floating point numbers 17 | R = float(R) 18 | v = float(v) 19 | m = float(m) 20 | C = float(C) 21 | 22 | return ((v / (v + m)) * R + (m / (v + m)) * C) 23 | 24 | 25 | def write_businesses(businesses): 26 | """ 27 | Returns a list of businesses for the API 28 | 29 | :param filename: Output file name 30 | :param businesses: List of businesses to write 31 | """ 32 | businesses.sort(key=lambda x: x.bayesian, reverse=True) 33 | result = [] 34 | for business in businesses: 35 | result.append( 36 | {'Location': 37 | '{},{}'.format(business.location[0], business.location[1]), 38 | 'Name': business.name, 39 | 'Rating': '{0:.2f}'.format(business.bayesian), 40 | 'Number_of_Ratings': business.rating_count, 41 | 'Sources': business.source_count}) 42 | return result 43 | 44 | 45 | def execute_search(locations, distance, query): 46 | """ 47 | Searches each API module at the given location(s) and distance. 48 | 49 | :param locations: User supplied lat/long point(s) 50 | :param distance: How far to search (meters) 51 | :param query: The niche, i.e. restaurants, bars, etc 52 | :returns: Full list of businesses 53 | """ 54 | full_business_list = [] 55 | for engine in [google, yelp]: 56 | businesses = [] 57 | for lat, lng in locations: 58 | businesses.extend(engine.search(lat, lng, distance, query)) 59 | # Remove duplicates from API call overlap 60 | names = set() 61 | filtered_list = [] 62 | print(time.strftime("%Y/%m/%d at %H:%M:%S ") + engine.__name__ + " " + str(len(businesses))) 63 | for business in businesses: 64 | if business: 65 | filtered_list.append(business) 66 | names.add(business.name) 67 | businesses = filtered_list 68 | # Calculate low threshold and average ratings 69 | try: 70 | low_threshold = min( 71 | business.rating_count for business in businesses) 72 | except: 73 | # go to next item 74 | continue 75 | average_rating = sum( 76 | business.rating for business in businesses) / len(businesses) 77 | # Convert to 10 point scale 78 | scale_multiplier = 2 79 | # Add bayesian estimates to business objects 80 | for business in businesses: 81 | business.bayesian = bayesian(business.rating * scale_multiplier, 82 | business.rating_count, 83 | low_threshold, 84 | average_rating * scale_multiplier) 85 | 86 | # Add this search engine's list to full business list 87 | full_business_list.extend(businesses) 88 | 89 | return full_business_list 90 | 91 | 92 | def combine_duplicate_businesses(businesses): 93 | """ 94 | Averages ratings of the same business from different sources 95 | 96 | :param businesses: Full list of businesses 97 | :returns: Filtered list with combined sources 98 | """ 99 | seen_addresses = set() 100 | filtered_list = [] 101 | for business in businesses: 102 | if business.address not in seen_addresses: 103 | filtered_list.append(business) 104 | seen_addresses.add(business.address) 105 | else: 106 | # Find duplicate in list 107 | for b in filtered_list: 108 | if b.address == business.address: 109 | # Average bayesian ratings and update source count 110 | new_rating = (b.bayesian + business.bayesian) / 2.0 111 | b.bayesian = new_rating 112 | b.source_count = b.source_count + 1 113 | 114 | return filtered_list 115 | --------------------------------------------------------------------------------