diff --git a/.gitignore b/.gitignore index 89981b0..8a0cf21 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ __pycache__/ *.pyc *.pyd /downloads/ +/browser/user_data/ +/browser/ diff --git a/backend/api/blueprints/reports/in_out_report.py b/backend/api/blueprints/reports/in_out_report.py index 0c92f72..26e8ba2 100644 --- a/backend/api/blueprints/reports/in_out_report.py +++ b/backend/api/blueprints/reports/in_out_report.py @@ -136,7 +136,7 @@ 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: SimpleCosecCredentialsHeaders(**x).model_dump(), + header_validator = lambda x: SimpleCosecCredentialsHeaders(**x), data_validator = lambda x: CosecInOutReportRequestData(**x) ) @handle_cancelled_request() @@ -156,8 +156,6 @@ async def in_out_report_generate( :return: A standard response structure. """ - current_app.printer("Hi") - # Start by assuming failure: success = False report_data = None @@ -165,7 +163,7 @@ async def in_out_report_generate( # Figure out the paths: base_dir = files.get_parent_directory( files.get_file_directory(include_filename = False), - depth = 1 + depth = 4 ) # Put together the directory for the drivers, the downloads, etc.: diff --git a/backend/api/blueprints/reports/muster_roll.py b/backend/api/blueprints/reports/muster_roll.py new file mode 100644 index 0000000..0e908ba --- /dev/null +++ b/backend/api/blueprints/reports/muster_roll.py @@ -0,0 +1,246 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Fri, 14th Nov, 2025 + + OBJECTIVE: + + To build APIs to serve Muster Roll Reports from Cosec's Web portal. + + 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 system-level activities: +import os + +# For using Quart: +from quart import Blueprint, current_app, request + +# My utils: +from utils_v2.string import json +from utils_v2.date_time import date_time +from utils_v2.system import files +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + 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 +) + +# Models: +from backend.models.api.common import SimpleCosecCredentialsHeaders +from backend.models.api.reports.muster_roll import CosecMusterRollReportRequestData + +# Cosec-related: +from cosec_web.cosec_web import CosecWeb + +# Common: +from backend.shared import constants + +# To work with dat and time: +import datetime + +# For asynchronous activities: +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +muster_roll_report_bp = Blueprint("muster_roll_report", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@muster_roll_report_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprints is registered. + # Consider this to be a one-time setup for the whole blueprints: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@muster_roll_report_bp.route("/generate", methods = ["GET"]) +@set_api_version(api_version = "1.0.0") +@read_input(sanitize_headers = False, sanitize_data = False) +# @get_session_info(key = "X-User-Id", session_coro = "get_session") +# @log_request_to_mongo( +# attr_name = "logs_mongo", +# project = constants.PROJECT_NAME, +# log_type = constants.MODULE_NAME, +# operation = "ytLnkAddApi", +# log_input = True, +# log_output = True, +# sensitive_keys = None +# ) +# @log_chain_to_mongo(attr_name = "logs_mongo") +@should_not_be_under_maintenance(attr_name = "is_under_maintenance") +@validate_input( + header_validator = lambda x: SimpleCosecCredentialsHeaders(**x), + data_validator = lambda x: CosecMusterRollReportRequestData(**x) +) +@handle_cancelled_request() +async def muster_roll_report_generate( + inbound_headers: SimpleCosecCredentialsHeaders | dict = None, + inbound_data: CosecMusterRollReportRequestData | dict = None, + inbound_files: dict = None, + **kwargs +): + + """ + To automate the web browser interaction with Cosec's web portal and return a JSON of the actual report content. + :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. + """ + + # Start by assuming failure: + success = False + report_data = None + + # Figure out the paths: + base_dir = files.get_parent_directory( + files.get_file_directory(include_filename = False), + depth = 4 + ) + + # Put together the directory for the drivers, the downloads, etc.: + chrome_driver_dir = os.path.join(base_dir, r"drivers/chrome") + user_data_dir = os.path.join(base_dir, r"browser/user_data") + downloads_dir = os.path.join(base_dir, r"downloads") + + # Show all the paths for debugging: + current_app.printer("PATHS:", chrome_driver_dir, user_data_dir, downloads_dir) + + # Create an instance of the automation object: + cosec = CosecWeb( + cosec_url = inbound_headers.cosecUrl, + username = inbound_headers.cosecUsername, + password = inbound_headers.cosecPassword, + driver_dir = chrome_driver_dir, + user_data_dir = user_data_dir, + downloads_dir = downloads_dir, + ) + + # Perform the login: + cosec.login(initial_sleep = 2.5) + + # Get the in/out report: + report_path = cosec.get_muster_roll( + initial_sleep = 1.0, + on_date = inbound_data.onDate, + group_ids = inbound_data.groupIds, + download_timeout = 60.0 + ) + + # Log out to end the cycle: + cosec.logout() + + # Close the browser window: + cosec.quit() + + # If we didn't get any path, the download failed: + if report_path is None: + success = False + + # Convert the In/Out Summary to a Pandas DF: + else: + success = True + report_df = cosec.read_muster_roll_xls(report_path) + report_df = report_df[[ + "User ID", "User Name", "Category Name", + "Grade Name", "Branch Name", "Department Name", + "Direct Reporting", "Level-1" + ]] + report_data = report_df.to_dict(orient = "records") + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + if success: return ResponseModel( + status_code = StatusCodes.OK, + http_code = HttpCodes.SUCCESS, + data = report_data, + message = f"Successfully fetched {len(report_data)} records." + ) + else: return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.INTERNAL_SERVER_ERROR, + message = f"Failed to fetch records. Please try again." + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/backend/api/main.py b/backend/api/main.py index a79610d..495f4d6 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -63,6 +63,7 @@ from icecream import IceCreamDebugger # All the blueprints: from backend.api.blueprints.reports.in_out_report import in_out_report_bp +from backend.api.blueprints.reports.muster_roll import muster_roll_report_bp # ***************************************************************************************************************** @@ -89,6 +90,7 @@ app = Quart(__name__) app = cors(app) # --- app.register_blueprint(in_out_report_bp, url_prefix = f"/{MODULE_BASE}/reports/in-out") +app.register_blueprint(muster_roll_report_bp, url_prefix = f"/{MODULE_BASE}/reports/muster-roll") # ***************************************************************************************************************** diff --git a/backend/models/api/common.py b/backend/models/api/common.py index 41a310d..18f90a2 100644 --- a/backend/models/api/common.py +++ b/backend/models/api/common.py @@ -76,7 +76,7 @@ class SimpleCosecCredentialsHeaders(BaseModel): cosecUrl: str = Field( description = "The URL to Cosec's portal.", frozen = True, - alias = "X-Cosec-URL" + alias = "X-Cosec-Url" ) cosecUsername: str = Field( diff --git a/backend/models/api/reports/muster_roll.py b/backend/models/api/reports/muster_roll.py new file mode 100644 index 0000000..89d5ab6 --- /dev/null +++ b/backend/models/api/reports/muster_roll.py @@ -0,0 +1,131 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Fri, 14th Nov, 2025. + + OBJECTIVE: + + To provide data model(s) for receiving API requests to get Muster Roll Reports. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# 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, AwareDatetime +from typing import Optional, Literal, List + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class CosecMusterRollReportRequestData(BaseModel): + + onDate: AwareDatetime | None = Field( + description = "The date when the report is desired.", + frozen = True, + default = None + ) + + groupIds: List[str] | str = Field( + description = "Each company/entity in Cosec's system is represented by a group id. This is a list of those ids.", + frozen = True, + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓┏ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗┛ + + @field_validator("onDate", mode = "before") + def parse_date_time(cls, value): + if value is None: value = date_time.get_current_utc_date_time(as_string = False) + value = date_time.parse_date_time( + input_value = value, + date_formats = [ + "%Y%m%d", + "%Y-%m-%d", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M:%S%z", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%dT%H:%M:%S%z", + ], + timezone = date_time.TIMEZONE_UTC + ) + return value + + @field_validator("groupIds", mode = "after") + def parse_group_ids(cls, value): + if isinstance(value, str): value = value.split(",") + return value + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/cosec_web/cosec_web.py b/cosec_web/cosec_web.py index ebbaf95..c1ff2eb 100644 --- a/cosec_web/cosec_web.py +++ b/cosec_web/cosec_web.py @@ -1197,42 +1197,42 @@ if __name__ == "__main__": print("User Data :", user_data_dir) print("Downloads :", downloads_dir) - # Create an instance of the automation object: - cosec = CosecWeb( - cosec_url = "http://103.89.8.21/cosec", - username = "hrd1", - password = "cosec", - driver_dir = chrome_driver_dir, - user_data_dir = user_data_dir, - downloads_dir = downloads_dir, - ) + # # Create an instance of the automation object: + # cosec = CosecWeb( + # cosec_url = "http://103.89.8.21/cosec", + # username = "hrd1", + # password = "cosec", + # driver_dir = chrome_driver_dir, + # user_data_dir = user_data_dir, + # downloads_dir = downloads_dir, + # ) + # + # # Perform the login: + # cosec.login(initial_sleep = 2.5) + # + # # Get the in/out report: + # in_out_report_path = cosec.get_in_out_summary( + # initial_sleep = 1.0, + # from_date = datetime.datetime.now() - datetime.timedelta(days = 1), + # to_date = datetime.datetime.now(), + # group_ids = [ + # "2", # ... Velankani Information Systems Pvt Ltd + # "3", # ... Velankani Bydesign India Pvt Ltd + # "4", # ... Velankani Electronics & Automotive Pvt Ltd + # ], + # download_timeout = 60.0 + # ) + # + # # Convert the In/Out Summary to a Pandas DF: + # # in_out_report_path = r"D:\programming\python\elcita_ofc_2025\downloads\Monthly_Details.xls" + # in_out_report_df = cosec.read_in_out_summary_xls(in_out_report_path) + # in_out_report_df = in_out_report_df[:35] + # print(in_out_report_df.to_string()) + # print("\n\n---\n\n") + # # print(in_out_report_df.info()) - # Perform the login: - cosec.login(initial_sleep = 2.5) - - # Get the in/out report: - in_out_report_path = cosec.get_in_out_summary( - initial_sleep = 1.0, - from_date = datetime.datetime.now() - datetime.timedelta(days = 1), - to_date = datetime.datetime.now(), - group_ids = [ - "2", # ... Velankani Information Systems Pvt Ltd - "3", # ... Velankani Bydesign India Pvt Ltd - "4", # ... Velankani Electronics & Automotive Pvt Ltd - ], - download_timeout = 60.0 - ) - - # Convert the In/Out Summary to a Pandas DF: - # in_out_report_path = r"D:\programming\python\elcita_ofc_2025\downloads\Monthly_Details.xls" - in_out_report_df = cosec.read_in_out_summary_xls(in_out_report_path) - in_out_report_df = in_out_report_df[:35] - print(in_out_report_df.to_string()) - print("\n\n---\n\n") - # print(in_out_report_df.info()) - - # Go back to the home page: - cosec.return_home() + # # Go back to the home page: + # cosec.return_home() # # Get the muster roll: # muster_roll_path = cosec.get_muster_roll( @@ -1246,20 +1246,25 @@ if __name__ == "__main__": # download_timeout = 60.0 # ) # - # # Convert the Muster Roll to a Pandas DF: - # # muster_roll_path = r"D:\programming\python\elcita_ofc_2025\downloads\Monthly_Details.xls" - # muster_roll_df = CosecWeb.read_muster_roll_xls(muster_roll_path) - # muster_roll_df = muster_roll_df[:35] - # print(muster_roll_df.to_string()) - # print("\n\n---\n\n") - # # print(muster_roll_df.info()) + # Convert the Muster Roll to a Pandas DF: + muster_roll_path = r"D:\kps\PycharmProjects\cosec\downloads\Monthly_Details.xls" + muster_roll_df = CosecWeb.read_muster_roll_xls(muster_roll_path) + muster_roll_df = muster_roll_df[[ + "User ID", "User Name", "Category Name", + "Grade Name", "Branch Name", "Department Name", + "Direct Reporting", "Level-1" + ]] + muster_roll_df = muster_roll_df[:35] + print(muster_roll_df.to_string()) + print("\n\n---\n\n") + print(muster_roll_df.info()) - # Log out to end the cycle: - cosec.logout() - - # Close the browser window: - cosec.quit() - - # Just to see what's going on, - # doesn't add to the operational requirements: - time.sleep(10.0) + # # Log out to end the cycle: + # cosec.logout() + # + # # Close the browser window: + # cosec.quit() + # + # # Just to see what's going on, + # # doesn't add to the operational requirements: + # time.sleep(10.0)