From 84efe75c068768f580cf93b212a376993112f449 Mon Sep 17 00:00:00 2001 From: yatmesh Date: Wed, 30 Jul 2025 10:05:46 +0530 Subject: [PATCH] (20250730) - ADDED - Google Places API for and paper trading update api with required fies and changes. --- .../finstitutions/trading/oauth/request.py | 213 +++++++++- .../software/google_places/__init__.py | 0 .../software/google_places/get_place.py | 366 ++++++++++++++++ api/blueprints/software/oauth/callback_v2.py | 8 +- api/blueprints/software/oauth/request_v2.py | 4 +- api/main.py | 2 + .../software/google_places/google_places.py | 6 +- .../api/finstitutions/trading/auth/oauth.py | 7 +- models/api/software/places/places.py | 390 ++++++++++++++++++ .../goog/controllers/places/places_client.py | 71 ++++ 10 files changed, 1054 insertions(+), 13 deletions(-) create mode 100644 api/blueprints/software/google_places/__init__.py create mode 100644 api/blueprints/software/google_places/get_place.py create mode 100644 models/api/software/places/places.py diff --git a/api/blueprints/finstitutions/trading/oauth/request.py b/api/blueprints/finstitutions/trading/oauth/request.py index 6711927..fefceb6 100644 --- a/api/blueprints/finstitutions/trading/oauth/request.py +++ b/api/blueprints/finstitutions/trading/oauth/request.py @@ -186,7 +186,10 @@ async def request_oauth_authorization_url( authType = "auth", user = kwargs["session_info"], clientUserId = { - "username": inbound_data.auth.username + "username": inbound_data.auth.username, + "perTrade": inbound_data.get("perTrade", 0), + "perCrore": inbound_data.get("perCrore", 0), + "perLot": inbound_data.get("perLot", 0) }, auth = inbound_data.auth.model_dump(), status = "active", @@ -311,6 +314,214 @@ async def request_oauth_authorization_url( ) + +# --------------------------------------------------------------------------------------------------------------------- + + +@trading_oauth_request_bp.route("/update", methods = ["GET", "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") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "tradingOAuthUrlReqApi", + log_input = True, + log_output = True, + sensitive_keys = ["sessionToken", "X-Session-Token"] +) +@log_chain_to_mongo(attr_name = "logs_mongo") +@should_not_be_under_maintenance(attr_name = "is_under_maintenance") +@validate_input( + header_validator = lambda x: TradingAuthRequestHeaders(**x).model_dump(), + data_validator = lambda x: TradingAuthRequestData(**x) +) +@handle_cancelled_request() +async def request_oauth_authorization_update_url( + inbound_headers: dict | TradingAuthRequestHeaders = None, + inbound_data: dict | TradingAuthRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this when requesting access to someone's trading account (like Zerodha). We generate a URL here which must be + opened by the user (typically in a separate tab), and the user must then grant access to his account directly on the + broker's site. The broker will then hit you with a callback URL when the user approves the request. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # ┏┓ + # ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏ + # ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛ + # ┛ + + # If the session token is invalid/expired: + if kwargs.get("session_info") is None: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED + ) + + # Start by assuming failure: + success = False + auth_url = None + + # ┏┓ ┏┓ ┏┳┓ ┓• + # ┣ ┏┓┏┓ ┃┃┏┓┏┓┏┓┏┓ ┃ ┏┓┏┓┏┫┓┏┓┏┓ + # ┻ ┗┛┛ ┣┛┗┻┣┛┗ ┛ ┻ ┛ ┗┻┗┻┗┛┗┗┫ + # ┛ ┛ + + # This is an internal paper-trading account. + # It won't need daily logins for usage. + + if inbound_data.client == "paperTrading": + + # Immediately save the details against that token id: + success = await current_app.paper_trading_controller.set_token( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + token_key=inbound_data.get("tokenKey"), + auth_token = CoreAuthTokenModel( + serviceType = "stockTrading", + client = inbound_data.client, + authType = "auth", + user = kwargs["session_info"], + clientUserId = { + "username": inbound_data.auth.username, + "perTrade": inbound_data.get("perTrade", 0), + "perCrore": inbound_data.get("perCrore", 0), + "perLot": inbound_data.get("perLot", 0) + }, + auth = inbound_data.auth.model_dump(), + status = "active", + syncFreq = None + ), + token_notes = { + "username": inbound_data.auth.username + }, + display_name = inbound_data.auth.username, + display_picture = None, + session_token = inbound_headers["X-Session-Token"] + ) + + # Check if things were successful: + if not success: auth_url = None + + # ┏┓ ┏┓ ┓┓ ┓┏┓• + # ┣ ┏┓┏┓ ┏┛┏┓┏┓┏┓┏┫┣┓┏┓ ┃┫ ┓╋┏┓ + # ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┛┗┗┻ ┛┗┛┗┗┗ + + # PLANNED FLOW FOR ZERODHA-KITE: + # Step 01.: (One time) The user will go to the integrations page and add his API Key and API Secret there. We store + # these values without verification. + # Step 02.: (Daily) The user will go to the investments tab and click on his Zerodha account, which will give him + # a URL that will take him to Zerodha's official site to log in. When he logs in, Zerodha will hit our + # callback URL and give us the authentication details. + + if inbound_data.client == "zerodhaKite": + + # Prepare the inputs: + auth_url = await current_app.zerodha_kite_controller.get_authorization_url(api_key = inbound_data.auth.apiKey) + + # Immediately save the details against that token id: + success = await current_app.zerodha_kite_controller.set_token_direct( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + auth_token = CoreAuthTokenModel( + serviceType = "stockTrading", + client = inbound_data.client, + authType = "oauth", + user = kwargs["session_info"], + clientUserId = { + "userId": inbound_data.auth.userId, + "apiKey": inbound_data.auth.apiKey + }, + auth = inbound_data.auth.model_dump(), + status = "active", + syncFreq = None + ), + token_notes = { + "userId": inbound_data.auth.userId, + "apiKey": inbound_data.auth.apiKey, + "authUrl": auth_url + }, + display_name = inbound_data.auth.userId, + display_picture = None, + session_token = inbound_headers["X-Session-Token"] + ) + + # Check if things were successful: + if not success: auth_url = None + + # ┏┓ ┳┏┓┳┏┓┳ ┳┓ + # ┣ ┏┓┏┓ ┃┃ ┃┃ ┃ ┣┫┏┓┏┓┏┓┓┏┓ + # ┻ ┗┛┛ ┻┗┛┻┗┛┻ ┻┛┛ ┗ ┗ ┗┗ + + # PLANNED FLOW FOR ICICI-BREEZE: + # Step 01.: (One time) The user will go to the integrations page and add his Client User ID, API Key, and API Secret + # there. We store these values without verification. + # Step 02.: (Daily) The user will go to the investments tab and click on his Breeze account, which will give him a + # URL that will take him to ICICI's official site to log in. When he logs in, ICICI will hit our callback + # URL and give us the authentication details. + + if inbound_data.client == "iciciBreeze": + + # Prepare the inputs: + auth_url = await current_app.icici_breeze_controller.get_authorization_url(api_key = inbound_data.auth.apiKey) + + # Immediately save the details against that token id: + success = await current_app.icici_breeze_controller.set_token_direct( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + auth_token = CoreAuthTokenModel( + serviceType = "stockTrading", + client = inbound_data.client, + authType = "oauth", + user = kwargs["session_info"], + clientUserId = { + "userId": inbound_data.auth.userId, + "apiKey": inbound_data.auth.apiKey + }, + auth = inbound_data.auth.model_dump(), + status = "active", + syncFreq = None + ), + token_notes = { + "userId": inbound_data.auth.userId, + "apiKey": inbound_data.auth.apiKey, + "authUrl": auth_url + }, + display_name = inbound_data.auth.userId, + display_picture = None, + session_token = inbound_headers["X-Session-Token"] + ) + + # Check if things were successful: + if not success: auth_url = None + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, + data = { + "client": inbound_data.client, + "authorizationUrl": auth_url + } + ) + + + # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** diff --git a/api/blueprints/software/google_places/__init__.py b/api/blueprints/software/google_places/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/software/google_places/get_place.py b/api/blueprints/software/google_places/get_place.py new file mode 100644 index 0000000..4a5416b --- /dev/null +++ b/api/blueprints/software/google_places/get_place.py @@ -0,0 +1,366 @@ +""" + + AUTHOR: + + Omkar Khandare + + DATE: + + Monday, 7th July., 2025. + + OBJECTIVE: + + To find places details. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, g, request + +# My utils: +from utils_v2.string import json +from utils_v2.database.async_mongo_v2 import AsyncMongo +from utils_v2.logging.context import AsyncLoggerContext +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + make_ordered_json, + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# google-Places-related utils: +# from utils_v2.goog.controllers.gmail.gmail_message import GmailMessage +from utils_v2.goog.controllers.places.places_client import AsyncPlacesClient +from utils_v2.goog.models.auth_tokens import GoogleAuthTokens + +# Common: +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.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 + +# For asynchronous activities: +import asyncio + +# To work with date and time: +import datetime + +# Helpers: +from api.helpers.user import token_check + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +google_places_bp = Blueprint("google_places", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@google_places_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@google_places_bp.route("/", 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") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "mailSendApi", + log_input = 1, + log_output = True, + sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKey", "tokenId"] +) +@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) +) +@handle_cancelled_request() +async def find_nearby( + inbound_headers: dict | MailSendRequestHeaders = None, + inbound_data: dict | MailSendRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this endpoint to find place nearby location. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # current_app.google_places_client + + # # ┏┓ ┓ ┏┓┓ ┓ + # # ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏ + # # ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗ + # + # # If the session token is invalid/expired: + # if ( + # kwargs.get("session_info") is None and + # inbound_headers["Remote-IP"] not in current_app.whitelisted_ips + # ): return constants.API_RESPONSE_UNAUTHORIZED + + # ┏┓ ┓ • ┏┓┓ ┓ + # ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏ + # ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗ + # ┛ + + # 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, + must_be_active = True + ) + if not auth_token: return constants.API_RESPONSE_NO_AUTH_TOKEN + + # Get the user's info: + if inbound_headers["Remote-IP"] not in current_app.whitelisted_ips: + user_info = CoreUserInfoModel(**kwargs["session_info"]) + else: user_info = auth_token.user + + # We check if the token that was used to fetch the mail is owned by this user: + if not await token_check.is_authorized( + mongo_data_conn = current_app.data_mongo, + user_info = user_info, + token_ids = [auth_token.authTokenId] + ): return constants.API_RESPONSE_UNAUTHORIZED + + # ┏┓ ┓ ┳┳┓ •┓ + # ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┓┃ + # ┗┛┗ ┛┗┗┻ ┛ ┗┗┻┗┗ + + # # Figure out the client connector: + # match auth_token.client: + # case "gmail": + # client_controller = current_app.places_controller + # client_connector = current_app.google_places_client + # case _: + # client_controller = None + # client_connector = None + # mail_message = None + # send_result.message = f"Invalid/unimplemented client '{auth_token.client}'" + + client_controller = current_app.places_controller + client_connector = current_app.google_places_client + + # If the controller and connector were matched: + if client_controller is not None and client_connector is not None: + send_result = await client_controller.nearby_radius_search( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + mail_client = client_connector, + mail_message = None, + auth_token = auth_token, + client_thread_id = inbound_data.clientThreadId, + user_info = user_info, + llm = current_app.llm, + session_token = inbound_headers["X-Session-Token"] + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if send_result.success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if send_result.success else HttpCodes.INTERNAL_SERVER_ERROR, + message = send_result.message + ) + + + +# --------------------------------------------------------------------------------------------------------------------- + + +@google_places_bp.route("/textsearch", 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") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "mailSendApi", + log_input = 1, + log_output = True, + sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKey", "tokenId"] +) +@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) +) +@handle_cancelled_request() +async def text_search( + inbound_headers: dict = None, + inbound_data: dict = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this endpoint to find place nearby location. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # current_app.google_places_client + + # # ┏┓ ┓ ┏┓┓ ┓ + # # ┣┫┓┏╋┣┓ ┃ ┣┓┏┓┏┃┏ + # # ┛┗┗┻┗┛┗ ┗┛┛┗┗ ┗┛┗ + # + # # If the session token is invalid/expired: + # if ( + # kwargs.get("session_info") is None and + # inbound_headers["Remote-IP"] not in current_app.whitelisted_ips + # ): return constants.API_RESPONSE_UNAUTHORIZED + + # ┏┓ ┓ • ┏┓┓ ┓ + # ┃┃┓┏┏┏┓┏┓┏┓┏┣┓┓┏┓ ┃ ┣┓┏┓┏┃┏ + # ┗┛┗┻┛┛┗┗ ┛ ┛┛┗┗┣┛ ┗┛┛┗┗ ┗┛┗ + # ┛ + + # 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"], + must_be_active = True + ) + print("AUTH:", auth_token) + if not auth_token: return constants.API_RESPONSE_NO_AUTH_TOKEN + + # Get the user's info: + if inbound_headers["Remote-IP"] not in current_app.whitelisted_ips: + user_info = CoreUserInfoModel(**kwargs["session_info"]) + else: user_info = auth_token.user + + # We check if the token that was used to fetch the mail is owned by this user: + if not await token_check.is_authorized( + mongo_data_conn = current_app.data_mongo, + user_info = user_info, + token_ids = [auth_token.authTokenId] + ): return constants.API_RESPONSE_UNAUTHORIZED + + # ┏┓ ┓ ┳┳┓ •┓ + # ┗┓┏┓┏┓┏┫ ┃┃┃┏┓┓┃ + # ┗┛┗ ┛┗┗┻ ┛ ┗┗┻┗┗ + + client_controller = current_app.places_controller + client_connector = current_app.google_places_client + + # 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( + tokens=auth_token, + text_query=inbound_data["textQuery"], + max_count=inbound_data["maxCount"] + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if send_result.success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if send_result.success else HttpCodes.INTERNAL_SERVER_ERROR, + message = send_result.message + ) + + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/blueprints/software/oauth/callback_v2.py b/api/blueprints/software/oauth/callback_v2.py index 0be4e36..8e4a97b 100644 --- a/api/blueprints/software/oauth/callback_v2.py +++ b/api/blueprints/software/oauth/callback_v2.py @@ -2,11 +2,11 @@ AUTHOR: - Khushal P Soonderji + Omkar Khandare DATE: - Thursday, 16th Jan., 2025. + Friday, 4th July., 2025. OBJECTIVE: @@ -20,10 +20,6 @@ N/A - NOTES: - - N/A - """ diff --git a/api/blueprints/software/oauth/request_v2.py b/api/blueprints/software/oauth/request_v2.py index 349eae9..3fa7985 100644 --- a/api/blueprints/software/oauth/request_v2.py +++ b/api/blueprints/software/oauth/request_v2.py @@ -2,11 +2,11 @@ AUTHOR: - Khushal P Soonderji + Omkar Khandare DATE: - Thursday, 16th jan., 2025. + Friday, 4th July., 2025. OBJECTIVE: diff --git a/api/main.py b/api/main.py index 73d5204..8af2955 100644 --- a/api/main.py +++ b/api/main.py @@ -142,6 +142,7 @@ from api.blueprints.message.chat.tags import chat_update_tags_bp from api.blueprints.software.auth import sw_auth_bp from api.blueprints.software.oauth.request_v2 import places_oauth_request_bp from api.blueprints.software.oauth.callback_v2 import places_oauth_callback_bp +from api.blueprints.software.google_places.get_place import google_places_bp # Finstitutions / Payment Blueprints: from api.blueprints.finstitutions.payments.auth_v2 import pg_auth_bp @@ -224,6 +225,7 @@ app.register_blueprint(chat_update_tags_bp, url_prefix = f"/{MODULE_BASE}/chat") app.register_blueprint(sw_auth_bp, url_prefix = f"/{MODULE_BASE}/software") app.register_blueprint(places_oauth_request_bp, url_prefix = f"/{MODULE_BASE}/software") app.register_blueprint(places_oauth_callback_bp, url_prefix = f"/{MODULE_BASE}/software") +app.register_blueprint(google_places_bp, url_prefix = f"/{MODULE_BASE}/software/googleplaces") # Finstitutions / Payment Blueprints: app.register_blueprint(pg_auth_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") diff --git a/controllers_v2/software/google_places/google_places.py b/controllers_v2/software/google_places/google_places.py index 21aac74..210276f 100644 --- a/controllers_v2/software/google_places/google_places.py +++ b/controllers_v2/software/google_places/google_places.py @@ -2,15 +2,15 @@ AUTHOR: - Khushal P Soonderji + Omkar Khandare DATE: - Thursday, 16th Jan., 2025. + Friday, 4th July., 2025. OBJECTIVE: - To handle all mail-related behaviour for Gmail from one place. + To handle all google-places-related behaviour for places API from one place. REFERENCES: diff --git a/models/api/finstitutions/trading/auth/oauth.py b/models/api/finstitutions/trading/auth/oauth.py index 004ca48..e8f73f6 100644 --- a/models/api/finstitutions/trading/auth/oauth.py +++ b/models/api/finstitutions/trading/auth/oauth.py @@ -117,7 +117,7 @@ class TradingAuthRequestData(BaseModel): # ┛ class Config: - extra = "forbid" + extra = "allow" # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ @@ -136,6 +136,11 @@ class TradingAuthRequestData(BaseModel): raise ValueError(f"incorrect 'auth' for selected client '{client}'") return values + # ADDED BY OMKAR - 2025-07-29 -------------------------------------------------------------------------------------- + + def get(self, key, default=None): + return getattr(self, key, default) + # ***************************************************************************************************************** # ***** **** diff --git a/models/api/software/places/places.py b/models/api/software/places/places.py new file mode 100644 index 0000000..cb55277 --- /dev/null +++ b/models/api/software/places/places.py @@ -0,0 +1,390 @@ +""" + + AUTHOR: + + Omkar Khandare + + DATE: + + Monday, 7th July., 2025. + + OBJECTIVE: + + To provide a structure to search places. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** +import io + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime, EmailStr, model_validator +from typing import Union, Literal, List + +# My utils: +from utils_v2.string import json +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + +# To work with Base64 data: +import base64 + +# To work with MongoDB: +from bson.objectid import ObjectId + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class PlacesRequestHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + default = None, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MailSendPlainText(BaseModel): + + content: str = Field( + description = "The string to add to the mail as plain text.", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MailSendHTMLText(BaseModel): + + content: str = Field( + description = "The HTML string to add to the mail.", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MailSendAttachment(BaseModel): + + content: str | io.BytesIO = Field( + description = "The Base64 string to add to the mail as a file.", + frozen = True + ) + + fileName: str = Field( + description = "The name of the file that will be downloaded when the recipient tries to access the content.", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + arbitrary_types_allowed = True + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("content", mode = "before") + def parse_base64_file(cls, value): + if isinstance(value, str): + base64_parts = value.split(",", 1) + if len(base64_parts) == 1: header, base64_string = None, base64_parts[0] + else: header, base64_string = base64_parts[0], base64_parts[1] + value = io.BytesIO(base64.b64decode(base64_string)) + return value + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MailSendInlineImage(BaseModel): + + content: str | io.BytesIO = Field( + description = "The image content to add to the mail as an inline image file.", + frozen = True + ) + + fileName: str = Field( + description = "The name of the file that will be downloaded when the recipient tries to access the content.", + frozen = True + ) + + cid: str | None = Field( + description = "A custom Content-Id to assign to the inline attachment.", + frozen = True, + default = None + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + arbitrary_types_allowed = True + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("content", mode = "before") + def parse_base64_file(cls, value): + if isinstance(value, str): + base64_parts = value.split(",", 1) + if len(base64_parts) == 1: header, base64_string = None, base64_parts[0] + else: header, base64_string = base64_parts[0], base64_parts[1] + value = io.BytesIO(base64.b64decode(base64_string)) + return value + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MailSendPart(BaseModel): + + type: Literal["plain", "html", "attachment", "inline"] = Field( + description = "The kind of part this is.", + frozen = True + ) + + part: dict | Union[ + MailSendPlainText, MailSendHTMLText, # ...... Textual content. + MailSendAttachment, MailSendInlineImage # ... Media content. + ] = Field( + description = "One of the structured types of data that can be put in the mail.", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @model_validator(mode = "before") + def ensure_harmony(cls, values): + kind_map = { + "plain": MailSendPlainText, + "html": MailSendHTMLText, + "attachment": MailSendAttachment, + "inline": MailSendInlineImage, + } + part_dict = values["part"] if isinstance(values["part"], dict) else values["part"].model_dump() + values["part"] = kind_map[values["type"]](**part_dict) + return values + + +# --------------------------------------------------------------------------------------------------------------------- + +class PlacesNearbyRequestData(BaseModel): + + tokenKey: ObjectId = Field( + description = "The identifier (Mongo ObjectId) of the account from which the mail has to be sent.", + frozen = True + ) + + latitude: float = Field( + description= "The Location latitude points", + frozen= True + ) + + longitude: float = Field( + description= "The Location longitude points", + frozen=True + ) + + radius: float = Field( + description= "The radius point of location your not provided by default will be 500 meter", + frozen=True, + default=500 + ) + + max_count: int = Field( + description= "Max count for get result counts", + frozen=True, + default=20 + ) + + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + arbitrary_types_allowed = True + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("tokenKey", mode = "before") + def parse_oid(cls, value): + try: value = ObjectId(value) + except: pass + return value + + # @field_validator("to", "cc", "bcc", mode = "before") + # def parse_recipients(cls, value): + # + # # Ensure that we are working with some kind of list: + # if value is None: value = [] + # if isinstance(value, str): value = [value] + # + # # # Ensure that all values of the list look like valid mails: + # # for index, email_id in enumerate(value): + # # if not regex.match( + # # text = email_id, + # # pattern = regex.REGEX_START + regex.REGEX_EMAIL_ID + regex.REGEX_END, + # # case_sensitive = False, + # # ): raise ValueError(f"'{email_id}' does not seem to be a valid e-mail id.") + # + # # Done here: + # return value + +# ---------------------------------------------------------------------------------------------------------------------- + + +class PlacesTextSearchRequestData(BaseModel): + + tokenKey: ObjectId = Field( + description = "The identifier (Mongo ObjectId) of the account from which the mail has to be sent.", + frozen = True + ) + + textQuery: float = Field( + description= "Text Query for find places", + frozen= True + ) + + max_count: int = Field( + description= "Max count for get result counts", + frozen=True, + default=20 + ) + + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + arbitrary_types_allowed = True + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("tokenKey", mode = "before") + def parse_oid(cls, value): + try: value = ObjectId(value) + except: pass + return value + + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/utils_v2/goog/controllers/places/places_client.py b/utils_v2/goog/controllers/places/places_client.py index 7ce5dbe..e321237 100644 --- a/utils_v2/goog/controllers/places/places_client.py +++ b/utils_v2/goog/controllers/places/places_client.py @@ -661,6 +661,77 @@ class AsyncPlacesClient(AsyncGoogleBase): 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 + """ + + # 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 + # } + # } + # } + + # 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 + + # 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:searchText", + 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 + + # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM ***