""" Delta Group JIRA-SharePoint Sync Manager This module handles the synchronization of JIRA tickets to SharePoint using the new Graph API-based connector architecture. """ import logging import csv import io from datetime import datetime, UTC from typing import Dict, Any, List, Optional from modules.connectors.connectorSharepoint import ConnectorSharepoint from modules.connectors.connectorTicketJira import ConnectorTicketJira from modules.interfaces.interfaceAppObjects import getRootInterface from modules.interfaces.interfaceAppModel import UserInDB from modules.interfaces.interfaceTicketObjects import TicketSharepointSyncInterface from modules.shared.timezoneUtils import get_utc_timestamp from modules.shared.configuration import APP_CONFIG logger = logging.getLogger(__name__) # Get environment type from configuration APP_ENV_TYPE = APP_CONFIG.get("APP_ENV_TYPE", "dev") class ManagerSyncDelta: """Manages JIRA to SharePoint synchronization for Delta Group. Supports two sync modes: - CSV mode: Uses CSV files for synchronization (default) - Excel mode: Uses Excel (.xlsx) files for synchronization To change sync mode, use the set_sync_mode() method or modify SYNC_MODE class variable. """ SHAREPOINT_SITE_ID = "02830618-4029-4dc8-8d3d-f5168f282249" SHAREPOINT_SITE_NAME = "SteeringBPM" SHAREPOINT_SITE_PATH = "SteeringBPM" SHAREPOINT_HOSTNAME = "deltasecurityag.sharepoint.com" SHAREPOINT_MAIN_FOLDER = "/General/50 Docs hosted by SELISE" SHAREPOINT_BACKUP_FOLDER = "/General/50 Docs hosted by SELISE/SyncHistory" SHAREPOINT_AUDIT_FOLDER = "/General/50 Docs hosted by SELISE/SyncHistory" SHAREPOINT_USER_ID = "patrick.motsch@delta.ch" # Sync mode: "csv" or "xlsx" SYNC_MODE = "xlsx" # Can be "csv" or "xlsx" # File names for different sync modes SYNC_FILE_CSV = "DELTAgroup x SELISE Ticket Exchange List.csv" SYNC_FILE_XLSX = "DELTAgroup x SELISE Ticket Exchange List.xlsx" # JIRA connection parameters (hardcoded for Delta Group) JIRA_USERNAME = "p.motsch@valueon.ch" JIRA_API_TOKEN = "ATATT3xFfGF0d973nNb3R1wTDI4lesmJfJAmooS-4cYMJTyLfwYv4himrE6yyCxyX3aSMfl34NHcm2fAXeFXrLHUzJx0RQVUBonCFnlgexjLQTgS5BoCbSO7dwAVjlcHZZkArHbooCUaRwJ15n6AHkm-nwdjLQ3Z74TFnKKUZC4uhuh3Aj-MuX8=2D7124FA" JIRA_URL = "https://deltasecurity.atlassian.net" JIRA_PROJECT_CODE = "DCS" JIRA_ISSUE_TYPE = "Task" # Task sync definition for field mapping (like original synchronizer) TASK_SYNC_DEFINITION={ #key=excel-header, [get:jira>excel | put: excel>jira, jira-xml-field-list] 'ID': ['get', ['key']], 'Module Category': ['get', ['fields', 'customfield_10058', 'value']], 'Summary': ['get', ['fields', 'summary']], 'Description': ['get', ['fields', 'description']], 'References': ['get', ['fields', 'customfield_10066']], 'Priority': ['get', ['fields', 'priority', 'name']], 'Issue Status': ['get', ['fields', 'customfield_10062']], 'Assignee': ['get', ['fields', 'assignee', 'displayName']], 'Issue Created': ['get', ['fields', 'created']], 'Due Date': ['get', ['fields', 'duedate']], 'DELTA Comments': ['get', ['fields', 'customfield_10060']], 'SELISE Ticket References': ['put', ['fields', 'customfield_10067']], 'SELISE Status Values': ['put', ['fields', 'customfield_10065']], 'SELISE Comments': ['put', ['fields', 'customfield_10064']], } def __init__(self): """Initialize the sync manager with hardcoded Delta Group credentials.""" self.root_interface = getRootInterface() self.jira_connector = None self.sharepoint_connector = None self.target_site = None def get_sync_file_name(self) -> str: """Get the appropriate sync file name based on the sync mode.""" if self.SYNC_MODE == "xlsx": return self.SYNC_FILE_XLSX else: # Default to CSV return self.SYNC_FILE_CSV def set_sync_mode(self, mode: str) -> bool: """Set the sync mode to either 'csv' or 'xlsx'. Args: mode: Either 'csv' or 'xlsx' Returns: bool: True if mode was set successfully, False if invalid mode """ if mode.lower() in ["csv", "xlsx"]: self.SYNC_MODE = mode.lower() logger.info(f"Sync mode changed to: {self.SYNC_MODE}") return True else: logger.error(f"Invalid sync mode: {mode}. Must be 'csv' or 'xlsx'") return False async def initialize_connectors(self) -> bool: """Initialize JIRA and SharePoint connectors.""" try: logger.info("Initializing JIRA connector with hardcoded credentials") # Initialize JIRA connector using class constants self.jira_connector = await ConnectorTicketJira.create( jira_username=self.JIRA_USERNAME, jira_api_token=self.JIRA_API_TOKEN, jira_url=self.JIRA_URL, project_code=self.JIRA_PROJECT_CODE, issue_type=self.JIRA_ISSUE_TYPE ) # Use the admin user for SharePoint connection adminUser = self.root_interface.getUserByUsername("admin") if not adminUser: logger.error("Admin user not found - SharePoint connection required") return False logger.info(f"Using admin user for SharePoint: {adminUser.id}") # Get SharePoint connection for admin user user_connections = self.root_interface.getUserConnections(adminUser.id) sharepoint_connection = None for connection in user_connections: if connection.authority == "msft" and connection.externalUsername == self.SHAREPOINT_USER_ID: sharepoint_connection = connection break if not sharepoint_connection: logger.error(f"No SharePoint connection found for user: {self.SHAREPOINT_USER_ID}") return False logger.info(f"Found SharePoint connection: {sharepoint_connection.id}") # Get SharePoint token for this connection sharepoint_token = self.root_interface.getConnectionToken(sharepoint_connection.id) if not sharepoint_token: logger.error("No SharePoint token found for Delta Group user connection") return False logger.info(f"Found SharePoint token: {sharepoint_token.id}") # Initialize SharePoint connector with Graph API self.sharepoint_connector = ConnectorSharepoint(access_token=sharepoint_token.tokenAccess) # Resolve the site by hostname + site path to get the real site ID logger.info( f"Resolving site ID via hostname+path: {self.SHAREPOINT_HOSTNAME}:/sites/{self.SHAREPOINT_SITE_PATH}" ) resolved = await self.sharepoint_connector.find_site_by_url( hostname=self.SHAREPOINT_HOSTNAME, site_path=self.SHAREPOINT_SITE_PATH ) if not resolved: logger.error( f"Failed to resolve site. Hostname: {self.SHAREPOINT_HOSTNAME}, Path: {self.SHAREPOINT_SITE_PATH}" ) return False self.target_site = { "id": resolved.get("id"), "displayName": resolved.get("displayName", self.SHAREPOINT_SITE_NAME), "name": resolved.get("name", self.SHAREPOINT_SITE_NAME) } # Test site access by listing root of the drive logger.info("Testing site access using resolved site ID...") test_result = await self.sharepoint_connector.list_folder_contents( site_id=self.target_site["id"], folder_path="" ) if test_result is not None: logger.info( f"Site access confirmed: {self.target_site['displayName']} (ID: {self.target_site['id']})" ) else: logger.error("Could not access site drive - check permissions") return False return True except Exception as e: logger.error(f"Error initializing connectors: {str(e)}") return False async def sync_jira_to_sharepoint(self) -> bool: """Perform the main JIRA to SharePoint synchronization using sophisticated sync logic.""" try: logger.info(f"Starting JIRA to SharePoint synchronization (Mode: {self.SYNC_MODE})") # Initialize connectors if not await self.initialize_connectors(): logger.error("Failed to initialize connectors") return False # Get the appropriate sync file name based on mode sync_file_name = self.get_sync_file_name() logger.info(f"Using sync file: {sync_file_name}") # Create the sophisticated sync interface sync_interface = await TicketSharepointSyncInterface.create( connector_ticket=self.jira_connector, connector_sharepoint=self.sharepoint_connector, task_sync_definition=self.TASK_SYNC_DEFINITION, sync_folder=self.SHAREPOINT_MAIN_FOLDER, sync_file=sync_file_name, backup_folder=self.SHAREPOINT_BACKUP_FOLDER, audit_folder=self.SHAREPOINT_AUDIT_FOLDER, site_id=self.target_site['id'] ) # Perform the sophisticated sync based on mode if self.SYNC_MODE == "xlsx": logger.info("Performing JIRA to Excel sync...") await sync_interface.sync_from_jira_to_excel() logger.info("Performing Excel to JIRA sync...") await sync_interface.sync_from_excel_to_jira() else: # CSV mode (default) logger.info("Performing JIRA to CSV sync...") await sync_interface.sync_from_jira_to_csv() logger.info("Performing CSV to JIRA sync...") await sync_interface.sync_from_csv_to_jira() logger.info(f"JIRA to SharePoint synchronization completed successfully (Mode: {self.SYNC_MODE})") return True except Exception as e: logger.error(f"Error during JIRA to SharePoint synchronization: {str(e)}") return False # Global sync function for use in app.py async def perform_sync_jira_delta_group() -> bool: """Perform JIRA to SharePoint synchronization for Delta Group. This function is called by the scheduler and can be used independently. Returns: bool: True if synchronization was successful, False otherwise """ try: if APP_ENV_TYPE != "prod": logger.info("JIRA to SharePoint synchronization: TASK to run only in PROD") return True logger.info("Starting Delta Group JIRA sync...") sync_manager = ManagerSyncDelta() success = await sync_manager.sync_jira_to_sharepoint() if success: logger.info("Delta Group JIRA sync completed successfully") else: logger.error("Delta Group JIRA sync failed") return success except Exception as e: logger.error(f"Error in perform_sync_jira_delta_group: {str(e)}") return False