(20250825) -Added new google places API Integration system, no more oauth system for places api integration,

added multiple files for and correction and changes as well.
This commit is contained in:
yatmesh
2025-08-25 14:43:14 +05:30
parent ddc29a7fe1
commit 7f7a16d4d2
8 changed files with 221 additions and 102 deletions
+23
View File
@@ -259,6 +259,29 @@ async def authorize_software_client(
else: else:
auth_url_failed = f"https://api.thecaoffice.com/shopify/auth/template?status=0&storeName={inbound_data.auth.storeName}&storeUrl={inbound_data.auth.storeUrl}" auth_url_failed = f"https://api.thecaoffice.com/shopify/auth/template?status=0&storeName={inbound_data.auth.storeName}&storeUrl={inbound_data.auth.storeUrl}"
elif inbound_data.softwareClient == "googlePlaces":
# Make the client controller test and save the auth:
response = await current_app.places_controller.save_auth(
sql_conn=current_app.sql_writer,
mongo_data_conn=current_app.data_mongo,
auth=inbound_data.auth,
user=kwargs.get("session_info"),
session_token=inbound_headers.get("X-Session-Token")
)
# Note down the results:
success = response.success
message = response.message
token_id = response.token_id
auth_url_success = ""
auth_url_failed = ""
if token_id is not None:
auth_url_success = f"https://api.thecaoffice.com/user/auth/template?status=1&client=googlePlaces"
else:
auth_url_failed = f"https://api.thecaoffice.com/user/auth/template?status=0&client=googlePlaces&message={message}"
# ┳┓ # ┳┓
# ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓
# ┛┗┗ ┛┣┛┗┛┛┗┛┗ # ┛┗┗ ┛┣┛┗┛┛┗┛┗
@@ -71,11 +71,10 @@ from utils_v2.goog.models.auth_tokens import GoogleAuthTokens
from shared import constants from shared import constants
# Data Models: # Data Models:
# from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData from models.api.software.places.places import PlacesRequestHeaders, PlacesTextSearchRequestData, PlacesNearbyRequestData
from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData
from models.message.mail.send import MailSendOneResult from models.message.mail.send import MailSendOneResult
from models.core.user import CoreUserInfoModel from models.core.user import CoreUserInfoModel
from models.core.auth_token import CoreAuthTokenModel
# To work with datatypes: # To work with datatypes:
from typing import Literal from typing import Literal
@@ -129,7 +128,7 @@ def init(blueprint_setup_state):
# --------------------------------------------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------------------------------------------
@google_places_bp.route("/<search_type>", methods = ["POST"]) @google_places_bp.route("/nearby", methods = ["POST"])
@set_api_version(api_version = "1.0.0") @set_api_version(api_version = "1.0.0")
@read_input(sanitize_headers = False, sanitize_data = False) @read_input(sanitize_headers = False, sanitize_data = False)
@get_session_info(key = "X-Session-Token", session_coro = "get_session") @get_session_info(key = "X-Session-Token", session_coro = "get_session")
@@ -145,13 +144,13 @@ def init(blueprint_setup_state):
@log_chain_to_mongo(attr_name = "logs_mongo") @log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance") @should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input( @validate_input(
header_validator = lambda x: MailSendRequestHeaders(**x).model_dump(), header_validator = lambda x: PlacesRequestHeaders(**x).model_dump(),
data_validator = lambda x: MailSendRequestData(**x) data_validator = lambda x: PlacesNearbyRequestData(**x)
) )
@handle_cancelled_request() @handle_cancelled_request()
async def find_nearby( async def find_nearby(
inbound_headers: dict | MailSendRequestHeaders = None, inbound_headers: dict | PlacesRequestHeaders = None,
inbound_data: dict | MailSendRequestData = None, inbound_data: dict | PlacesNearbyRequestData = None,
inbound_files: dict = None, inbound_files: dict = None,
**kwargs **kwargs
): ):
@@ -267,13 +266,13 @@ async def find_nearby(
@log_chain_to_mongo(attr_name = "logs_mongo") @log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance") @should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input( @validate_input(
header_validator = lambda x: MailSendRequestHeaders(**x).model_dump(), header_validator = lambda x: PlacesRequestHeaders(**x).model_dump(),
data_validator = lambda x: MailSendRequestData(**x) data_validator = lambda x: PlacesTextSearchRequestData(**x)
) )
@handle_cancelled_request() @handle_cancelled_request()
async def text_search( async def text_search(
inbound_headers: dict = None, inbound_headers: dict | PlacesRequestHeaders = None,
inbound_data: dict = None, inbound_data: dict | PlacesTextSearchRequestData = None,
inbound_files: dict = None, inbound_files: dict = None,
**kwargs **kwargs
): ):
@@ -303,11 +302,11 @@ async def text_search(
# ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏ # ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏
# ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗ # ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗
# ┛ # ┛
print(inbound_data)
# Get the token based on the key: # Get the token based on the key:
auth_token = await current_app.places_controller.get_token_from_key( auth_token = await current_app.places_controller.get_token_from_key(
mongo_data_conn = current_app.data_mongo, mongo_data_conn = current_app.data_mongo,
token_key = inbound_data["tokenKey"], token_key = inbound_data.tokenKey,
must_be_active = True must_be_active = True
) )
print("AUTH:", auth_token) print("AUTH:", auth_token)
@@ -334,10 +333,10 @@ async def text_search(
# If the controller and connector were matched: # If the controller and connector were matched:
if client_controller is not None and client_connector is not None: if client_controller is not None and client_connector is not None:
send_result = await client_controller.text_query_search( send_result = await client_connector.text_query_search(
tokens=auth_token, tokens=auth_token,
text_query=inbound_data["textQuery"], text_query=inbound_data.textQuery,
max_count=inbound_data["maxCount"] max_count=inbound_data.maxCount
) )
# ┳┓ # ┳┓
+10 -76
View File
@@ -52,20 +52,12 @@ from utils_v2.rest.models.api_call import ApiResponse
from models.core.user import CoreUserInfoModel from models.core.user import CoreUserInfoModel
from models.core.auth_token import CoreAuthTokenModel from models.core.auth_token import CoreAuthTokenModel
from models.core.user import CoreUserInfoModel from models.core.user import CoreUserInfoModel
from models.api.message.mail.oauth import ( from models.api.software.places.oauth import PlacesAuthResponse
OAuthMailAuthorizationRequestHeaders,
OAuthMailAuthorizationRequestData
)
from models.message.mail.oauth import OAuthMailGetAuthorizationURLResponse, OAuthMailHandleCallbackResponse
from models.core.message import CoreMessageModel
# To make HTTP requests: # To make HTTP requests:
import httpx import httpx
# to work with MongoDB:
from bson.objectid import ObjectId
# Mail Client(s):
from utils_v2.goog.controllers.places.places_client import AsyncPlacesClient
# To make abstract classes: # To make abstract classes:
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@@ -173,80 +165,22 @@ class GooglePlacesController(CoreSoftwareController, ABC):
debug_only_errors = debug_only_errors debug_only_errors = debug_only_errors
) )
# ┏┓┏┓ ┓ ┏┓ ┏┓
# ┃┃┣┫┓┏╋┣┓┏┛ ┃┫
# ┗┛┛┗┗┻┗┛┗┗━•┗┛
@abstractmethod @abstractmethod
async def get_authorization_url( async def save_auth(
self, self,
sql_conn: AsyncMySQL, sql_conn: AsyncMySQL,
mongo_data_conn: AsyncMongo, mongo_data_conn: AsyncMongo,
mail_client: AsyncPlacesClient, auth: CoreAuthTokenModel,
user_info: CoreUserInfoModel, user: CoreUserInfoModel,
inbound_data: OAuthMailAuthorizationRequestData,
session_token: str session_token: str
) -> OAuthMailGetAuthorizationURLResponse: ) -> PlacesAuthResponse:
""" """
To accept an incoming request for mail integration and provide a URL that the user can use to authorize your Checks if a particular set of incoming credentials give access to a valid server and then stores the
service to access his mail inbox. credentials.
:param sql_conn: The database connection to use to perform this task. :param sql_conn: The database connection to use to perform this task.
:param mongo_data_conn: The database connection to use to perform this task. :param mongo_data_conn: The database connection to use to perform this task.
:param mail_client: The instance of the third-party mail client that will be used to get the URL. :param auth: The set of credentials as received from the UI/API.
:param user_info: The information about your user who is trying to use this system. :return: A structured response to indicate what happened during authorization.
:param inbound_data: The data that came in with the request (API call).
:param session_token: The session token of the user.
:return: A structure response with details about the URL generation process.
"""
pass
@abstractmethod
async def handle_authorization_callback(
self,
sql_conn: AsyncMySQL,
mongo_data_conn: AsyncMongo,
mail_client: AsyncPlacesClient,
request_url: str,
inbound_data: dict,
session_token: str = None
) -> OAuthMailHandleCallbackResponse:
"""
To handle the authorization callback for the mail client. The user may grant or deny authorization.
:param sql_conn: The database connection to use to perform this task.
:param mongo_data_conn: The database connection to use to perform this task.
:param mail_client: The instance of the third-party mail client that will be used to get the URL.
:param request_url: The full callback URL invoked by the third-party client.
:param inbound_data: The data that came in with the request (API call).
:param session_token: The session token of the user. It is expected that this will be null in all cases.
:return: A structured response of the process of handling the mail callback.
"""
pass
@abstractmethod
async def refresh_authorization(
self,
sql_conn: AsyncMySQL,
mongo_data_conn: AsyncMongo,
mail_client: AsyncPlacesClient,
http_client: httpx.AsyncClient,
auth_token: CoreAuthTokenModel,
force_refresh: bool = False,
session_token: str = None
) -> CoreAuthTokenModel:
"""
To refresh the third-party client's access/authorization token(s) before use.
:param sql_conn: The database connection to use when storing the refreshed tokens.
:param mongo_data_conn: The database connection to use when storing the refreshed tokens.
:param mail_client: The connection of the third-party mail client.
:param http_client: The HTTP client to use to make the token refresh request.
:param auth_token: The auth-token model of the existing integration. This may get updated if a refresh is needed
(or forced).
:param force_refresh: Whether, or not, you would like to force a refresh even if the token hasn't expired yet.
:param session_token: The session token of the user. This will be null if this method is invoked by a cron
script in the background. Needed only to identify the user in case of a failure to send a timely alert.
:return: The same auth-token model instance, but maybe with updated tokens.
""" """
pass pass
@@ -34,7 +34,7 @@
import sys import sys
sys.path.append(".") sys.path.append(".")
sys.path.append("..") sys.path.append("..")
import aiohttp
# My async utils: # My async utils:
from utils_v2.string import json from utils_v2.string import json
from utils_v2.mail import mail_parser from utils_v2.mail import mail_parser
@@ -53,7 +53,8 @@ from models.core.auth_token import CoreAuthTokenModel
from models.core.message import CoreMessageModel from models.core.message import CoreMessageModel
from models.api.software.places.oauth import ( from models.api.software.places.oauth import (
OAuthPlacesAuthorizationRequestHeaders, OAuthPlacesAuthorizationRequestHeaders,
OAuthPlacesAuthorizationRequestData OAuthPlacesAuthorizationRequestData,
PlacesAuthResponse
) )
from models.software.places.oauth import OAuthPlacesGetAuthorizationURLResponse, OAuthPlacesHandleCallbackResponse from models.software.places.oauth import OAuthPlacesGetAuthorizationURLResponse, OAuthPlacesHandleCallbackResponse
@@ -168,6 +169,81 @@ class PlacesController(GooglePlacesController):
# Init a variable in a parent: # Init a variable in a parent:
self._client = self.CLIENT_NAME self._client = self.CLIENT_NAME
async def is_google_places_api_key_valid(api_key: str) -> bool:
url = "https://places.googleapis.com/v1/places:searchText"
headers = {
"Content-Type": "application/json",
"X-Goog-Api-Key": api_key,
"X-Goog-FieldMask": "places.displayName" # minimal required field
}
payload = {
"textQuery": "Google Sydney" # dummy search text
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=payload, timeout=10) as response:
if response.status != 200:
return False
data = await response.json()
# If key invalid, response will contain "error"
if "error" in data:
return False
return True
except Exception:
return False
async def save_auth(
self,
sql_conn: AsyncMySQL,
mongo_data_conn: AsyncMongo,
auth: CoreAuthTokenModel ,
user: CoreUserInfoModel,
session_token: str
) -> PlacesAuthResponse:
# CHECK -- API KEY IS VALID -
valid_api = await PlacesController.is_google_places_api_key_valid(api_key=auth.apiKey)
if valid_api:
success, object_id = await self.set_token_direct_with_return_id(
sql_conn=sql_conn,
mongo_data_conn=mongo_data_conn,
auth_token=CoreAuthTokenModel(
serviceType=self.SERVICE_TYPE,
client=self.CLIENT_NAME,
authType="auth",
auth=auth.model_dump(),
user=user,
clientUserId={
"email": auth.email,
"apiKey": auth.apiKey
},
status="active",
syncFreq=60
),
token_notes={
"email": auth.email,
"apiKey": auth.apiKey
},
display_name=auth.displayName,
display_picture=None,
session_token=session_token
)
# Done here:
return PlacesAuthResponse(
success=success,
token_id=str(object_id),
message="Places Account Added successfully." if success else "Places Account Added failed."
)
else:
return PlacesAuthResponse(
success=False,
token_id="",
message="Invalid Google Places API KEY, please provide valid API KEY. Thank You"
)
# ┏┓┏┓ ┓ ┏┓ ┏┓ # ┏┓┏┓ ┓ ┏┓ ┏┓
# ┃┃┣┫┓┏╋┣┓┏┛ ┃┫ # ┃┃┣┫┓┏╋┣┓┏┛ ┃┫
# ┗┛┛┗┗┻┗┛┗┗━•┗┛ # ┗┛┛┗┗┻┗┛┗┗━•┗┛
+5 -3
View File
@@ -43,6 +43,7 @@ from typing import Optional, Literal, Union
from models.software.tcaoff_ai.auth import TheCAOfficeAIAuth from models.software.tcaoff_ai.auth import TheCAOfficeAIAuth
from models.software.mikrotik.auth import MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth from models.software.mikrotik.auth import MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth
from models.software.ecommerce.auth import ShopifyAuth from models.software.ecommerce.auth import ShopifyAuth
from models.software.places.oauth import GooglePlacesAuth
# My utils: # My utils:
from utils_v2.string import regex from utils_v2.string import regex
from utils_v2.date_time import date_time from utils_v2.date_time import date_time
@@ -105,8 +106,8 @@ class SoftwareAuthRequestHeaders(BaseModel):
class SoftwareAuthRequestData(BaseModel): class SoftwareAuthRequestData(BaseModel):
softwareClient: Literal["theCaOfficeAi", "mikrotikPPPoE1000", "mikrotikHotspot1000", "shopify"] = Field(alias = "client") softwareClient: Literal["theCaOfficeAi", "mikrotikPPPoE1000", "mikrotikHotspot1000", "shopify", "googlePlaces"] = Field(alias = "client")
auth: Union[TheCAOfficeAIAuth, MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth, ShopifyAuth] auth: Union[TheCAOfficeAIAuth, MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth, ShopifyAuth, GooglePlacesAuth]
# ┏┓ ┏• # ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓ # ┃ ┏┓┏┓╋┓┏┓
@@ -128,7 +129,8 @@ class SoftwareAuthRequestData(BaseModel):
"theCaOfficeAi": TheCAOfficeAIAuth, "theCaOfficeAi": TheCAOfficeAIAuth,
"mikrotikPPPoE1000": MikroTikPPPoE1000Auth, "mikrotikPPPoE1000": MikroTikPPPoE1000Auth,
"mikrotikHotspot1000": MikroTikHotspot1000Auth, "mikrotikHotspot1000": MikroTikHotspot1000Auth,
"shopify": ShopifyAuth "shopify": ShopifyAuth,
"googlePlaces": GooglePlacesAuth
} }
if not isinstance(auth, harmony_map[client]): if not isinstance(auth, harmony_map[client]):
raise ValueError(f"incorrect 'auth' for selected client '{client}'") raise ValueError(f"incorrect 'auth' for selected client '{client}'")
+51 -1
View File
@@ -38,7 +38,7 @@ sys.path.append("..")
# For making data behaviour_models: # For making data behaviour_models:
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from typing import Optional, Literal from typing import Optional, Literal, Any
# My utils: # My utils:
from utils_v2.string import regex from utils_v2.string import regex
@@ -123,6 +123,56 @@ class OAuthPlacesAuthorizationRequestData(BaseModel):
pass pass
# ----------------------------------------------------------------------------------------------------------------------
class PlacesAuthResponse(BaseModel):
success: bool = Field(
description = "To indicate whether or not, the action was a success",
frozen = False,
default = False
)
token_id : str = Field(
description="MongoDb object id",
frozen=False,
default=False
)
message: str = Field(
description = "To explain what happened in the process of handling the OAuth callback.",
frozen = False,
default = "ERR: Message not captured."
)
exception: Any = Field(
description = "To pass on any exception that occurred in the process.",
frozen = False,
default = None
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
# ┏┓ ┏┓
# ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏
# ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛
pass
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
pass
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
# *** MAIN PROGRAM *** # *** MAIN PROGRAM ***
+3 -3
View File
@@ -345,15 +345,15 @@ class PlacesTextSearchRequestData(BaseModel):
frozen = True frozen = True
) )
textQuery: float = Field( textQuery: str = Field(
description= "Text Query for find places", description= "Text Query for find places",
frozen= True frozen= True
) )
max_count: int = Field( maxCount: int = Field(
description= "Max count for get result counts", description= "Max count for get result counts",
frozen=True, frozen=True,
default=20 default=5
) )
+35
View File
@@ -173,6 +173,41 @@ class OAuthPlacesHandleCallbackResponse(BaseModel):
pass pass
class GooglePlacesAuth(BaseModel):
email: str = Field(
description="Email for google places API",
frozen=False
)
apiKey: str = Field(
description="API KEY of Places API",
frozen=True,
)
displayName: str = Field(
description="Display Name",
frozen=True,
)
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
# ┗┛┗┛┛┗┛┗┗┫
# ┛
class Config:
extra = "forbid"
# ┏┓ ┏┓
# ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏
# ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛
pass
# ┓┏ ┓• ┓ •
# ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓
# ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗
pass
# ***************************************************************************************************************** # *****************************************************************************************************************
# ***** **** # ***** ****
# *** MAIN PROGRAM *** # *** MAIN PROGRAM ***