(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:
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
# Data Models:
# from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData
from models.api.message.mail.send import MailSendRequestHeaders, MailSendRequestData
from models.api.software.places.places import PlacesRequestHeaders, PlacesTextSearchRequestData, PlacesNearbyRequestData
from models.message.mail.send import MailSendOneResult
from models.core.user import CoreUserInfoModel
from models.core.auth_token import CoreAuthTokenModel
# To work with datatypes:
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")
@read_input(sanitize_headers = False, sanitize_data = False)
@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")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input(
header_validator = lambda x: MailSendRequestHeaders(**x).model_dump(),
data_validator = lambda x: MailSendRequestData(**x)
header_validator = lambda x: PlacesRequestHeaders(**x).model_dump(),
data_validator = lambda x: PlacesNearbyRequestData(**x)
)
@handle_cancelled_request()
async def find_nearby(
inbound_headers: dict | MailSendRequestHeaders = None,
inbound_data: dict | MailSendRequestData = None,
inbound_headers: dict | PlacesRequestHeaders = None,
inbound_data: dict | PlacesNearbyRequestData = None,
inbound_files: dict = None,
**kwargs
):
@@ -267,13 +266,13 @@ async def find_nearby(
@log_chain_to_mongo(attr_name = "logs_mongo")
@should_not_be_under_maintenance(attr_name = "is_under_maintenance")
@validate_input(
header_validator = lambda x: MailSendRequestHeaders(**x).model_dump(),
data_validator = lambda x: MailSendRequestData(**x)
header_validator = lambda x: PlacesRequestHeaders(**x).model_dump(),
data_validator = lambda x: PlacesTextSearchRequestData(**x)
)
@handle_cancelled_request()
async def text_search(
inbound_headers: dict = None,
inbound_data: dict = None,
inbound_headers: dict | PlacesRequestHeaders = None,
inbound_data: dict | PlacesTextSearchRequestData = None,
inbound_files: dict = None,
**kwargs
):
@@ -303,11 +302,11 @@ async def text_search(
# ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏
# ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗
# ┛
print(inbound_data)
# Get the token based on the key:
auth_token = await current_app.places_controller.get_token_from_key(
mongo_data_conn = current_app.data_mongo,
token_key = inbound_data["tokenKey"],
token_key = inbound_data.tokenKey,
must_be_active = True
)
print("AUTH:", auth_token)
@@ -334,10 +333,10 @@ async def text_search(
# If the controller and connector were matched:
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,
text_query=inbound_data["textQuery"],
max_count=inbound_data["maxCount"]
text_query=inbound_data.textQuery,
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.auth_token import CoreAuthTokenModel
from models.core.user import CoreUserInfoModel
from models.api.message.mail.oauth import (
OAuthMailAuthorizationRequestHeaders,
OAuthMailAuthorizationRequestData
)
from models.message.mail.oauth import OAuthMailGetAuthorizationURLResponse, OAuthMailHandleCallbackResponse
from models.core.message import CoreMessageModel
from models.api.software.places.oauth import PlacesAuthResponse
# To make HTTP requests:
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:
from abc import ABC, abstractmethod
@@ -173,80 +165,22 @@ class GooglePlacesController(CoreSoftwareController, ABC):
debug_only_errors = debug_only_errors
)
# ┏┓┏┓ ┓ ┏┓ ┏┓
# ┃┃┣┫┓┏╋┣┓┏┛ ┃┫
# ┗┛┛┗┗┻┗┛┗┗━•┗┛
@abstractmethod
async def get_authorization_url(
async def save_auth(
self,
sql_conn: AsyncMySQL,
mongo_data_conn: AsyncMongo,
mail_client: AsyncPlacesClient,
user_info: CoreUserInfoModel,
inbound_data: OAuthMailAuthorizationRequestData,
auth: CoreAuthTokenModel,
user: CoreUserInfoModel,
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
service to access his mail inbox.
Checks if a particular set of incoming credentials give access to a valid server and then stores the
credentials.
: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 user_info: The information about your user who is trying to use this system.
: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.
:param auth: The set of credentials as received from the UI/API.
:return: A structured response to indicate what happened during authorization.
"""
pass
@@ -34,7 +34,7 @@
import sys
sys.path.append(".")
sys.path.append("..")
import aiohttp
# My async utils:
from utils_v2.string import json
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.api.software.places.oauth import (
OAuthPlacesAuthorizationRequestHeaders,
OAuthPlacesAuthorizationRequestData
OAuthPlacesAuthorizationRequestData,
PlacesAuthResponse
)
from models.software.places.oauth import OAuthPlacesGetAuthorizationURLResponse, OAuthPlacesHandleCallbackResponse
@@ -168,6 +169,81 @@ class PlacesController(GooglePlacesController):
# Init a variable in a parent:
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.mikrotik.auth import MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth
from models.software.ecommerce.auth import ShopifyAuth
from models.software.places.oauth import GooglePlacesAuth
# My utils:
from utils_v2.string import regex
from utils_v2.date_time import date_time
@@ -105,8 +106,8 @@ class SoftwareAuthRequestHeaders(BaseModel):
class SoftwareAuthRequestData(BaseModel):
softwareClient: Literal["theCaOfficeAi", "mikrotikPPPoE1000", "mikrotikHotspot1000", "shopify"] = Field(alias = "client")
auth: Union[TheCAOfficeAIAuth, MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth, ShopifyAuth]
softwareClient: Literal["theCaOfficeAi", "mikrotikPPPoE1000", "mikrotikHotspot1000", "shopify", "googlePlaces"] = Field(alias = "client")
auth: Union[TheCAOfficeAIAuth, MikroTikPPPoE1000Auth, MikroTikHotspot1000Auth, ShopifyAuth, GooglePlacesAuth]
# ┏┓ ┏•
# ┃ ┏┓┏┓╋┓┏┓
@@ -128,7 +129,8 @@ class SoftwareAuthRequestData(BaseModel):
"theCaOfficeAi": TheCAOfficeAIAuth,
"mikrotikPPPoE1000": MikroTikPPPoE1000Auth,
"mikrotikHotspot1000": MikroTikHotspot1000Auth,
"shopify": ShopifyAuth
"shopify": ShopifyAuth,
"googlePlaces": GooglePlacesAuth
}
if not isinstance(auth, harmony_map[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:
from pydantic import BaseModel, Field, field_validator
from typing import Optional, Literal
from typing import Optional, Literal, Any
# My utils:
from utils_v2.string import regex
@@ -123,6 +123,56 @@ class OAuthPlacesAuthorizationRequestData(BaseModel):
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 ***
+3 -3
View File
@@ -345,15 +345,15 @@ class PlacesTextSearchRequestData(BaseModel):
frozen = True
)
textQuery: float = Field(
textQuery: str = Field(
description= "Text Query for find places",
frozen= True
)
max_count: int = Field(
maxCount: int = Field(
description= "Max count for get result counts",
frozen=True,
default=20
default=5
)
+35
View File
@@ -173,6 +173,41 @@ class OAuthPlacesHandleCallbackResponse(BaseModel):
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 ***