From b0a5f1e27296f28ed46e27b434e2bd7e76dedec0 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Fri, 10 Jan 2020 22:51:07 +0100 Subject: [PATCH] Added Foursquare integration - closes #94 --- docs/source/platypush/plugins/foursquare.rst | 5 + docs/source/plugins.rst | 1 + platypush/plugins/foursquare.py | 359 +++++++++++++++++++ 3 files changed, 365 insertions(+) create mode 100644 docs/source/platypush/plugins/foursquare.rst create mode 100644 platypush/plugins/foursquare.py diff --git a/docs/source/platypush/plugins/foursquare.rst b/docs/source/platypush/plugins/foursquare.rst new file mode 100644 index 0000000000..5af214179b --- /dev/null +++ b/docs/source/platypush/plugins/foursquare.rst @@ -0,0 +1,5 @@ +``platypush.plugins.foursquare`` +================================ + +.. automodule:: platypush.plugins.foursquare + :members: diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 704598f454..686b3cc970 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -26,6 +26,7 @@ Plugins platypush/plugins/db.rst platypush/plugins/dropbox.rst platypush/plugins/file.rst + platypush/plugins/foursquare.rst platypush/plugins/google.rst platypush/plugins/google.calendar.rst platypush/plugins/google.drive.rst diff --git a/platypush/plugins/foursquare.py b/platypush/plugins/foursquare.py new file mode 100644 index 0000000000..2e02e5bc89 --- /dev/null +++ b/platypush/plugins/foursquare.py @@ -0,0 +1,359 @@ +import datetime +import requests +from typing import List, Dict, Any, Optional, Union, Tuple + +from platypush.plugins import Plugin, action + + +class FoursquarePlugin(Plugin): + """ + Plugin to interact with the `Foursquare Places API `_. + + In order to enable the Foursquare API on your account you need to: + + - Create a new app on the `Foursquare developers website `_. + - Copy the ``client_id`` and ``client_secret``. + - Add a redirect URL. It must point to a valid IP/hostname with a web server running, even if it runs + locally. You can also use the local URL of the platypush web server - e.g. http://192.168.1.2:8008/. + - Open the following URL: ``https://foursquare.com/oauth2/authenticate?client_id=CLIENT_ID&response_type=token&redirect_uri=REDIRECT_URI``. + Replace ``CLIENT_ID`` and ``REDIRECT_URI`` with the parameters from your app. + - Allow the application. You will be redirected to the URL you provided. Copy the ``access_token`` provided in + the URL. + + """ + + api_base_url = 'https://api.foursquare.com/v2' + + def __init__(self, access_token: str, **kwargs): + """ + :param access_token: + """ + super().__init__(**kwargs) + self.access_token = access_token + + def _get_url(self, endpoint): + return '{url}/{endpoint}?oauth_token={token}&v={version}'.format( + url=self.api_base_url, endpoint=endpoint, token=self.access_token, + version=datetime.date.today().strftime('%Y%m%d'), + ) + + @action + def get_checkins(self) -> List[Dict[str, Any]]: + """ + Get the list of check-ins of the current user. + :return: A list of checkins, as returned by the Foursquare API. + """ + url = self._get_url('users/self/checkins') + return requests.get(url).json().get('response', {}).get('checkins', {}).get('items', []) + + # noinspection DuplicatedCode + @action + def search(self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + altitude: Optional[float] = None, + latlng_accuracy: Optional[float] = None, + altitude_accuracy: Optional[float] = None, + near: Optional[str] = None, + query: Optional[str] = None, + limit: Optional[int] = None, + url: Optional[int] = None, + categories: Optional[List[str]] = None, + radius: Optional[int] = None, + sw: Optional[Union[Tuple[float], List[float]]] = None, + ne: Optional[Union[Tuple[float], List[float]]] = None,) -> List[Dict[str, Any]]: + """ + Search for venues. + + :param latitude: Search near this latitude. Note either ``latitude, longitude`` or ``near`` should be provided. + :param longitude: Search near this latitude. Note either ``latitude, longitude`` or ``near`` should be provided. + :param near: Search near this place (e.g. "Chicago, IL" or "Amsterdam, NL"). Note either + ``latitude, longitude`` or ``near`` should be provided. + :param altitude: Search near this altitude in meters. + :param latlng_accuracy: Latitude/longitude accuracy in meters. + :param altitude_accuracy: Altitude accuracy in meters. + :param query: Search query (e.g. "coffee shops" or "restaurants"). + :param limit: Maximum number of results. + :param url: Venue URL to search. + :param categories: List of ``category IDs ``_ + to be searched. + :param radius: Search radius in meters. + :param sw: South/west boundary box as a ``[latitude, longitude]`` pair. + :param ne: North/east boundary box as a ``[latitude, longitude]`` pair. + :return: A list of venues, as returned by the Foursquare API. + """ + assert (latitude and longitude) or near, 'Specify either latitude/longitude or near' + args = {} + + if latitude and longitude: + args['ll'] = ','.join([str(latitude), str(longitude)]) + if near: + args['near'] = near + if altitude: + args['alt'] = altitude + if latlng_accuracy: + args['llAcc'] = latlng_accuracy + if altitude_accuracy: + args['altAcc'] = altitude_accuracy + if query: + args['query'] = query + if limit: + args['limit'] = int(limit) + if url: + args['url'] = url + if categories: + args['categoryId'] = ','.join(categories) + if radius: + args['radius'] = radius + if sw: + args['sw'] = sw + if ne: + args['ne'] = ne + + url = self._get_url('venues/search') + return requests.get(url, params=args).json().get('response', {}).get('venues', []) + + # noinspection DuplicatedCode + @action + def explore(self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + altitude: Optional[float] = None, + latlng_accuracy: Optional[float] = None, + altitude_accuracy: Optional[float] = None, + section: Optional[str] = None, + near: Optional[str] = None, + query: Optional[str] = None, + limit: Optional[int] = None, + categories: Optional[List[str]] = None, + radius: Optional[int] = None, + open_now: bool = True, + sort_by_distance: Optional[bool] = None, + sort_by_popularity: Optional[bool] = None, + price: Optional[List[int]] = None, + saved: Optional[bool] = None) -> List[Dict[str, Any]]: + """ + Explore venues around a location. + + :param latitude: Search near this latitude. Note either ``latitude, longitude`` or ``near`` should be provided. + :param longitude: Search near this latitude. Note either ``latitude, longitude`` or ``near`` should be provided. + :param near: Search near this place (e.g. "Chicago, IL" or "Amsterdam, NL"). Note either + ``latitude, longitude`` or ``near`` should be provided. + :param altitude: Search near this altitude in meters. + :param latlng_accuracy: Latitude/longitude accuracy in meters. + :param altitude_accuracy: Altitude accuracy in meters. + :param section: Section to search. Supported values: + + - food + - drinks + - coffee + - shops + - arts + - outdoors + - sights + - trending + - nextVenues + + :param query: Search query (e.g. "coffee shops" or "restaurants"). The parameter has no effect if + ``section`` is specified. + :param limit: Maximum number of results. + :param categories: List of ``category IDs ``_ + to be searched. + :param radius: Search radius in meters. + :param open_now: Filter by open/not open now. + :param sort_by_distance: Sort by distance. + :param sort_by_popularity: Sort by popularity + :param price: Price ranges, within the range ``[1,2,3,4]``. + :param saved: Filter by saved/unsaved venues. + + :return: A list of venues, as returned by the Foursquare API. + """ + assert (latitude and longitude) or near, 'Specify either latitude/longitude or near' + args = {} + + if latitude and longitude: + args['ll'] = ','.join([str(latitude), str(longitude)]) + if near: + args['near'] = near + if altitude: + args['alt'] = altitude + if latlng_accuracy: + args['llAcc'] = latlng_accuracy + if altitude_accuracy: + args['altAcc'] = altitude_accuracy + if section: + args['section'] = section + if query: + args['query'] = query + if categories: + args['categoryId'] = ','.join(categories) + if limit: + args['limit'] = int(limit) + if radius: + args['radius'] = radius + if open_now is not None: + args['openNow'] = int(open_now) + if sort_by_distance is not None: + args['sortByDistance'] = int(sort_by_distance) + if sort_by_popularity is not None: + args['sortByPopularity'] = sort_by_popularity + if saved is not None: + args['saved'] = int(saved) + if price: + args['price'] = ','.join([str(p) for p in price]) + + url = self._get_url('venues/explore') + return requests.get(url, params=args).json().get('response', {}).get('venues', []) + + @action + def trending(self, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + near: Optional[str] = None, + limit: Optional[int] = None, + radius: Optional[int] = None) -> List[Dict[str, Any]]: + """ + Get the trending venues around a location. + + :param latitude: Search near this latitude. Note either ``latitude, longitude`` or ``near`` should be provided. + :param longitude: Search near this latitude. Note either ``latitude, longitude`` or ``near`` should be provided. + :param near: Search near this place (e.g. "Chicago, IL" or "Amsterdam, NL"). Note either + ``latitude, longitude`` or ``near`` should be provided. + :param limit: Maximum number of results. + :param radius: Search radius in meters. + + :return: A list of venues, as returned by the Foursquare API. + """ + assert (latitude and longitude) or near, 'Specify either latitude/longitude or near' + args = {} + + if latitude and longitude: + args['ll'] = ','.join([str(latitude), str(longitude)]) + if near: + args['near'] = near + if limit: + args['limit'] = int(limit) + if radius: + args['radius'] = radius + + url = self._get_url('venues/trending') + return requests.get(url, params=args).json().get('response', {}).get('venues', []) + + @staticmethod + def _parse_time(t): + if isinstance(t, int) or isinstance(t, float): + return datetime.datetime.fromtimestamp(t) + if isinstance(t, str): + return datetime.datetime.fromisoformat(t) + + assert isinstance(t, datetime.datetime), 'Cannot parse object of type {} into datetime: {}'.format( + type(t), t) + return t + + @action + def time_series(self, + venue_id: Union[str, List[str]], + start_at: Union[int, float, datetime.datetime, str], + end_at: Union[int, float, datetime.datetime, str]) -> List[Dict[str, Any]]: + """ + Get the visitors stats about one or multiple venues over a time range. The user must be a manager of + those venues. + + :param venue_id: Venue ID or list of IDs to get the stats for. + :param start_at: Stats start time. Can be a UNIX timestamp, a datetime object or an ISO format datetime. + :param end_at: Stats end time. Can be a UNIX timestamp, a datetime object or an ISO format datetime. + + :return: A list of venues, as returned by the Foursquare API. + """ + if isinstance(venue_id, list): + venue_id = ','.join(venue_id) + + args = { + 'venueId': venue_id, + 'startAt': self._parse_time(start_at), + 'endAt': self._parse_time(end_at), + } + + url = self._get_url('venues/timeseries') + return requests.get(url, params=args).json().get('response', {}).get('venues', []) + + @action + def stats(self, + venue_id: str, + start_at: Union[int, float, datetime.datetime, str], + end_at: Union[int, float, datetime.datetime, str]) -> List[Dict[str, Any]]: + """ + Get the stats about a venue over a time range. The user must be a manager of that venue. + + :param venue_id: Venue ID. + :param start_at: Stats start time. Can be a UNIX timestamp, a datetime object or an ISO format datetime. + :param end_at: Stats end time. Can be a UNIX timestamp, a datetime object or an ISO format datetime. + + :return: A list of venues, as returned by the Foursquare API. + """ + args = { + 'startAt': self._parse_time(start_at), + 'endAt': self._parse_time(end_at), + } + + url = self._get_url('venues/{}/stats'.format(venue_id)) + return requests.get(url, params=args).json().get('response', {}).get('venues', []) + + @action + def managed(self) -> List[Dict[str, Any]]: + """ + Get the list of venues managed by the user. + :return: A list of venues, as returned by the Foursquare API. + """ + url = self._get_url('venues/managed') + return requests.get(url).json().get('response', {}).get('venues', []).get('items', []) + + @action + def checkin(self, + venue_id: str, + latitude: Optional[float] = None, + longitude: Optional[float] = None, + altitude: Optional[float] = None, + latlng_accuracy: Optional[float] = None, + altitude_accuracy: Optional[float] = None, + shout: Optional[str] = None, + broadcast: Optional[List[str]] = None) -> Dict[str, Any]: + """ + Create a new check-in. + + :param venue_id: ID of the venue to check-in. + :param latitude: Check-in latitude. + :param longitude: Check-in longitude. + :param altitude: Check-in altitude. + :param latlng_accuracy: Latitude/longitude accuracy in meters. + :param altitude_accuracy: Altitude accuracy in meters. + :param shout: Add a custom message to the check-in. + :param broadcast: List of Visibility/share types of the check-in. Default: ``public``. Possible values are: + + - ``private`` + - ``public`` + - ``followers`` + - ``facebook`` + - ``twitter`` + + :return: Foursquare API response. + """ + args = {'venueId': venue_id} + if latitude and longitude: + args['ll'] = ','.join([str(latitude), str(longitude)]) + if altitude: + args['alt'] = altitude + if latlng_accuracy: + args['llAcc'] = latlng_accuracy + if altitude_accuracy: + args['altAcc'] = altitude_accuracy + if shout: + args['shout'] = shout + if broadcast: + args['broadcast'] = ','.join(broadcast) if isinstance(broadcast, list) else broadcast + + url = self._get_url('checkins/add') + return requests.post(url, data=args).json().get('response', {}).get('checkin', {}) + + +# vim:sw=4:ts=4:et: