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: