Squashed 'utils_v2/' content from commit 62600ef

git-subtree-dir: utils_v2
git-subtree-split: 62600ef57051e69587da18015b7b6840fdac3194
This commit is contained in:
2025-11-14 17:00:28 +05:30
commit 7fdd2411bb
227 changed files with 151811 additions and 0 deletions
View File
+831
View File
@@ -0,0 +1,831 @@
"""
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())