""" AUTHOR: Khushal P Soonderji DATE: Friday, 27th Jun., 2025 OBJECTIVE: To get location and business info from Google's Places API. REFERENCES: 1. GMail Quickstart: https://developers.google.com/gmail/api/quickstart/python 2. GMail Labels: https://developers.google.com/gmail/api/guides/labels 3. GMail Messages: https://developers.google.com/gmail/api/reference/rest/v1/users.messages 4. People Profile: https://developers.google.com/people/api/rest/v1/people/get DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # System-level activities: import io # My utils: from utils_v2.string import json # My Google utils: from utils_v2.goog.controllers.base import AsyncGoogleBase from utils_v2.goog.models.auth_tokens import GoogleAuthTokens from utils_v2.goog.models.api_call import GoogleApiResponse # Related to Google: from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from googleapiclient.discovery import build # To make API calls: import httpx # For asynchronous activities: import asyncio # To work with date and time: import datetime # For working with datatypes: from typing import Dict, Literal, List, Any # For debugging: from icecream import IceCreamDebugger import inspect # For computational help: import math # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # Google Scopes: SCOPES_PLACES_FULL = [ r"https://www.googleapis.com/auth/cloud-platform", r"https://www.googleapis.com/auth/userinfo.profile", r"https://www.googleapis.com/auth/gmail.metadata" ] # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** CLASSES *** # ***** **** # ***************************************************************************************************************** class AsyncPlacesClient(AsyncGoogleBase): # The types of places: PLACE_TYPES = { "Automotive": [ "car_dealer", "car_rental", "car_repair", "car_wash", "electric_vehicle_charging_station", "gas_station", "parking", "rest_stop" ], "Business": [ "corporate_office", "farm", "ranch" ], "Culture": [ "art_gallery", "art_studio", "auditorium", "cultural_landmark", "historical_place", "monument", "museum", "performing_arts_theater", "sculpture" ], "Education": [ "library", "preschool", "primary_school", "secondary_school", "university" ], "Entertainment and Recreation": [ "adventure_sports_center", "amphitheatre", "amusement_center", "amusement_park", "aquarium", "banquet_hall", "barbecue_area", "botanical_garden", "bowling_alley", "casino", "childrens_camp", "comedy_club", "community_center", "concert_hall", "convention_center", "cultural_center", "cycling_park", "dance_hall", "dog_park", "event_venue", "ferris_wheel", "garden", "hiking_area", "historical_landmark", "internet_cafe", "karaoke", "marina", "movie_rental", "movie_theater", "national_park", "night_club", "observation_deck", "off_roading_area", "opera_house", "park", "philharmonic_hall", "picnic_ground", "planetarium", "plaza", "roller_coaster", "skateboard_park", "state_park", "tourist_attraction", "video_arcade", "visitor_center", "water_park", "wedding_venue", "wildlife_park", "wildlife_refuge", "zoo" ], "Facilities": [ "public_bath", "public_bathroom", "stable" ], "Finance": [ "accounting", "atm", "bank" ], "Food and Drink": [ "acai_shop", "afghani_restaurant", "african_restaurant", "american_restaurant", "asian_restaurant", "bagel_shop", "bakery", "bar", "bar_and_grill", "barbecue_restaurant", "brazilian_restaurant", "breakfast_restaurant", "brunch_restaurant", "buffet_restaurant", "cafe", "cafeteria", "candy_store", "cat_cafe", "chinese_restaurant", "chocolate_factory", "chocolate_shop", "coffee_shop", "confectionery", "deli", "dessert_restaurant", "dessert_shop", "diner", "dog_cafe", "donut_shop", "fast_food_restaurant", "fine_dining_restaurant", "food_court", "french_restaurant", "greek_restaurant", "hamburger_restaurant", "ice_cream_shop", "indian_restaurant", "indonesian_restaurant", "italian_restaurant", "japanese_restaurant", "juice_shop", "korean_restaurant", "lebanese_restaurant", "meal_delivery", "meal_takeaway", "mediterranean_restaurant", "mexican_restaurant", "middle_eastern_restaurant", "pizza_restaurant", "pub", "ramen_restaurant", "restaurant", "sandwich_shop", "seafood_restaurant", "spanish_restaurant", "steak_house", "sushi_restaurant", "tea_house", "thai_restaurant", "turkish_restaurant", "vegan_restaurant", "vegetarian_restaurant", "vietnamese_restaurant", "wine_bar" ], "Geographical Areas": [ "administrative_area_level_1", "administrative_area_level_2", "country", "locality", "postal_code", "school_district" ], "Government": [ "city_hall", "courthouse", "embassy", "fire_station", "government_office", "police", "post_office" ], "Health and Wellness": [ "chiropractor", "dental_clinic", "dentist", "doctor", "drugstore", "hospital", "massage", "medical_lab", "pharmacy", "physiotherapist", "sauna", "skin_care_clinic", "spa", "tanning_studio", "wellness_center", "yoga_studio" ], "Housing": [ "apartment_building", "apartment_complex", "condominium_complex", "housing_complex" ], "Lodging": [ "bed_and_breakfast", "budget_japanese_inn", "campground", "camping_cabin", "cottage", "extended_stay_hotel", "farmstay", "guest_house", "hostel", "hotel", "inn", "japanese_inn", "lodging", "mobile_home_park", "motel", "private_guest_room", "resort_hotel", "rv_park" ], "Natural Features": [ "beach" ], "Places of Worship": [ "church", "hindu_temple", "mosque", "synagogue" ], "Services": [ "astrologer", "barber_shop", "beautician", "beauty_salon", "body_art_service", "catering_service", "cemetery", "child_care_agency", "consultant", "courier_service", "electrician", "florist", "food_delivery", "foot_care", "funeral_home", "hair_care", "hair_salon", "insurance_agency", "laundry", "lawyer", "locksmith", "makeup_artist", "moving_company", "nail_salon", "painter", "plumber", "psychic", "real_estate_agency", "roofing_contractor", "storage", "summer_camp_organizer", "tailor", "telecommunications_service_provider", "tour_agency", "tourist_information_center", "travel_agency", "veterinary_care" ], "Shopping": [ "asian_grocery_store", "auto_parts_store", "bicycle_store", "book_store", "butcher_shop", "cell_phone_store", "clothing_store", "convenience_store", "department_store", "discount_store", "electronics_store", "food_store", "furniture_store", "gift_shop", "grocery_store", "hardware_store", "home_improvement_store", "jewelry_store", "market", "pet_store", "shoe_store", "shopping_mall", "sporting_goods_store", "store", "supermarket", "warehouse_store", "wholesaler" ], "Sports": [ "arena", "athletic_field", "fishing_charter", "fishing_pond", "fitness_center", "golf_course", "gym", "ice_skating_rink", "playground", "ski_resort", "sports_activity_location", "sports_club", "sports_coaching", "sports_complex", "stadium", "swimming_pool" ], "Transportation": [ "airport", "airstrip", "bus_station", "bus_stop", "ferry_terminal", "heliport", "international_airport", "light_rail_station", "park_and_ride", "subway_station", "taxi_stand", "train_station", "transit_depot", "transit_station", "truck_stop" ] } # Kinds of fields: FIELD_MASK_BASIC_INFO = [ "nextPageToken", "places.id", "places.name", "places.displayName", "places.formattedAddress", "places.location", "places.googleMapsUri", "places.photos", "places.primaryType", "places.types" ] FIELD_MASK_BUSINESS_LEADS = [ "nextPageToken", "places.id", "places.name", "places.displayName", "places.businessStatus", "places.formattedAddress", "places.postalAddress", "places.location" "places.googleMapsUri", "places.photos", "places.primaryType", "places.types", "places.regularOpeningHours", "places.regularSecondaryOpeningHours", "places.rating", "places.userRatingCount", "places.websiteUri", "places.internationalPhoneNumber", "places.priceLevel", "places.priceRange" ] # ┳┳ ┏┓ ┏•┓ # ┃┃┏┏┓┏┓ ┃┃┏┓┏┓╋┓┃┏┓ # ┗┛┛┗ ┛ ┣┛┛ ┗┛┛┗┗┗ async def get_user_profile( self, tokens: GoogleAuthTokens, user_id: str = "me", ) -> GoogleApiResponse: """ To get the details of the user that has logged in. DOCUMENTATION: 1. https://developers.google.com/gmail/api/reference/rest/v1/users/getProfile 2. https://developers.google.com/people/api/rest/v1/people/get 3. https://developers.google.com/people/api/rest/v1/people#Person :param tokens: The object that holds the access token to the service. :param user_id: The id of the user for which this service must be run. Typically, the e-mail id itself or "me". :return: A structured response where the list of labels will be in the 'data' variable. """ # Ensure that the tokens are valid: await tokens.arefresh( http_client=self._http_client, client_id=self._client_id, client_secret=self._client_secret, force_refresh=False ) # Make the GMail API call: if not self._debug_only_errors: self._printer("Getting User Profile.") gmail_api_response = await self.get( url=f"https://gmail.googleapis.com/gmail/v1/users/{user_id}/profile", headers={"Authorization": f"Bearer {tokens.accessToken}"} ) # If the call was successful: if gmail_api_response.httpCode in [200]: gmail_api_response.success = True gmail_api_response.data = await gmail_api_response.get_json() gmail_api_response.data["displayName"] = None gmail_api_response.data["displayPictureUrl"] = None # If the call failed: else: self._printer( gmail_api_response.action, gmail_api_response.method, gmail_api_response.httpCode, ) return gmail_api_response # Make the People API call: if not self._debug_only_errors: self._printer("Getting User Profile.") people_api_response = await self.get( url=f"https://people.googleapis.com/v1/people/me?personFields=names,photos,birthdays,phoneNumbers,genders,emailAddresses,addresses", headers={"Authorization": f"Bearer {tokens.accessToken}"} ) # If the call was successful: if people_api_response.httpCode in [200]: people_api_response.success = True people_api_response.data = await people_api_response.get_json() for item in people_api_response.data.get("names", []): if item["metadata"]["primary"]: gmail_api_response.data["displayName"] = item.get("displayName") for item in people_api_response.data.get("photos", []): if item["metadata"]["primary"]: gmail_api_response.data["displayPictureUrl"] = item.get("url") # If the call failed: else: self._printer( people_api_response.action, people_api_response.method, people_api_response.httpCode, ) # Done here: return gmail_api_response # ┏┓ • # ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏ # ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛ # ┛ @property def primary_types(self): return list(self.PLACE_TYPES.keys()) def types_for(self, primary_type: str): return self.PLACE_TYPES.get(primary_type) # async def _nearby_radius_search( # self, # tokens: GoogleAuthTokens, # latitude: float, # longitude: float, # radius: float, # max_count: int = 100, # next_page_token: str = None, # ) -> GoogleApiResponse: async def nearby_radius_search( self, tokens: GoogleAuthTokens, latitude: float, longitude: float, radius: float, included_primary_types: List[str] = None, included_types: List[str] = None, excluded_primary_types: List[str] = None, excluded_types: List[str] = None, field_mask: List[str] = None, max_count: int = 100, next_page_token: str = None, ) -> GoogleApiResponse: """ https://developers.google.com/maps/documentation/places/web-service/nearby-search """ # Ensure that the tokens are valid: await tokens.arefresh( http_client = self._http_client, client_id = self._client_id, client_secret = self._client_secret, force_refresh = False ) # Create the headers: request_headers = {"Authorization": f"Bearer {tokens.accessToken}"} if field_mask: request_headers["X-Goog-FieldMask"] = ",".join(field_mask) # Start creating the JSON payload based on the inputs: request_json = { "maxResultCount": max_count, "locationRestriction": { "circle": { "center": { "latitude": latitude, "longitude": longitude }, "radius": radius } } } if included_primary_types: request_json["includedPrimaryTypes"] = included_primary_types if included_types: request_json["includedTypes"] = included_types if excluded_primary_types: request_json["excludedPrimaryTypes"] = excluded_primary_types if excluded_types: request_json["excludedTypes"] = excluded_types # Make the API call: if not self._debug_only_errors: self._printer("Listing Nearby (Radius) Places.") api_response = await self.post( url = f"https://places.googleapis.com/v1/places:searchNearby", headers = request_headers, json = request_json ) # If the call was successful: if api_response.httpCode in [200]: api_response.success = True api_response.data = await api_response.get_json() # Done here: return api_response async def text_query_search( self, tokens: GoogleAuthTokens, text_query: str, included_primary_types: List[str] = None, included_types: List[str] = None, excluded_primary_types: List[str] = None, excluded_types: List[str] = None, field_mask: List[str] = None, max_count: int = 100, next_page_token: str = None, ) -> GoogleApiResponse: """ https://developers.google.com/maps/documentation/places/web-service/text-search """ print("IN TOKEN", tokens) print("IN TOKEN", tokens.model_dump()) print("IN TOKEN", tokens.token["accessToken"]) # Ensure that the tokens are valid: # await tokens.arefresh( # http_client=self._http_client, # client_id=self._client_id, # client_secret=self._client_secret, # force_refresh=False # ) print("RH --") # Create the headers: request_headers = {"Authorization": f"Bearer {tokens.token['accessToken']}"} # if field_mask: request_headers["X-Goog-FieldMask"] = ",".join(field_mask) print("RH",request_headers) if field_mask: request_headers["X-Goog-FieldMask"] = "places.displayName,places.formattedAddress,places.priceLevel" # Start creating the JSON payload based on the inputs: # request_json = { # "maxResultCount": max_count, # "locationRestriction": { # "circle": { # "center": { # "latitude": latitude, # "longitude": longitude # }, # "radius": radius # } # } # } # Start creating the JSON payload based on the inputs: request_json = { "maxResultCount": max_count, "textQuery": text_query } # if included_primary_types: request_json["includedPrimaryTypes"] = included_primary_types # if included_types: request_json["includedTypes"] = included_types # if excluded_primary_types: request_json["excludedPrimaryTypes"] = excluded_primary_types # if excluded_types: request_json["excludedTypes"] = excluded_types print(request_json) # Make the API call: if not self._debug_only_errors: self._printer("Listing text Search Places.") api_response = await self.post( url=f"https://places.googleapis.com/v1/places:searchText", headers=request_headers, json=request_json ) print(api_response) # If the call was successful: if api_response.httpCode in [200]: api_response.success = True api_response.data = await api_response.get_json() # Done here: return api_response # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": # Create an HTTP client: test_client = httpx.AsyncClient( limits = httpx.Limits( max_connections = 100, # ............ Maximum number of connections allowed in the pool. max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive. ), timeout = httpx.Timeout( connect = 2.5, # ... Shorter connection timeout. read = 2.5, # ...... Like what EasyEcom gives. write = 10.0, # .... Time to wait for sending data. pool = 120.0 # ..... Time to wait for a free connection from the pool. ) ) # Read the secrets that give you access to the app: # secrets_file = r"C:\Users\Khushal P Soonderji\Downloads\google_places_test_secret.json" secrets_file = r"/home/python-dev-debug/Downloads/client_secret_349360248417-uuba8eudk75jg1jag212g5obhc1uostk.apps.googleusercontent.com.json" secrets_dict = json.from_file(secrets_file) async def main(): # Create an instance of the client: my_goog = AsyncPlacesClient( service_name = "places", oauth_json = secrets_dict, http_client = test_client, redirect_url = r"https://wtt.ditscentre.in/shopify/test/1", debug = True, debug_prefix = "Places (M) | ", debug_only_errors = False ) # Request Auth: print("AUTH URL:", await my_goog.get_authorization_url( scopes = SCOPES_PLACES_FULL, state = "Bhopli", approval_prompt = "force" )) # Get tokens from callback: test_tokens = await my_goog.get_authorization_tokens( scopes = SCOPES_PLACES_FULL, redirect_url = input("Paste the redirect URL here: ") ) print("TOKENS:", test_tokens) # List out the primary and secondary types: print("PRIMARY TYPES:", my_goog.primary_types) print("'Food and Drink' TYPES:", my_goog.types_for("Food and Drink")) print("'Alien Spaceship' TYPES:", my_goog.types_for("Alien Spaceship")) response = await my_goog.get_user_profile( tokens=test_tokens ) print("SUCCESS:", response.success) print("SUMMARY:", response.to_markdown()) print("\n\n---\n\n") print("DATA:", json.to_string(response.data, default=str)) if not response.success: print("\n\n---\n\n") # Test some feature: response = await my_goog.nearby_radius_search( tokens = test_tokens, latitude = 19.03145183334671, longitude = 72.85437426861148, radius = 1_000.0, field_mask = None, max_count = 5, ) print("SUCCESS:", response.success) print("SUMMARY:", response.to_markdown()) print("\n\n---\n\n") print("DATA:", json.to_string(response.data, default = str)) if not response.success: print("\n\n---\n\n") asyncio.run(main())