From 34195eb5e845fe10b978b59b16f29aab9acc9536 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Mon, 29 Apr 2024 14:52:28 +0100 Subject: [PATCH 01/25] setting up design --- odins_spear/reporter.py | 28 +++++- odins_spear/reports/call_flow.py | 57 ++++++++++++ .../reports/report_utils/graphviz_module.py | 86 +++++++++++++++++++ odins_spear/reports/report_utils/parsing.py | 9 ++ 4 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 odins_spear/reports/call_flow.py create mode 100644 odins_spear/reports/report_utils/graphviz_module.py create mode 100644 odins_spear/reports/report_utils/parsing.py diff --git a/odins_spear/reporter.py b/odins_spear/reporter.py index af1f0ad..3edb3c0 100644 --- a/odins_spear/reporter.py +++ b/odins_spear/reporter.py @@ -1,6 +1,28 @@ +from . import reports class Reporter: - ''' generates human friendly reports - ''' + """ generates human friendly reports. + """ + def __init__(self, api) -> None: - self.api = api \ No newline at end of file + self.api = api + + + def call_flow(self, service_provider_id: str, group_id: str, number: str, number_type: str, + broadworks_entity_type: str): + """Generates a graphical flow chart of how a call to a specified number routes + through the broadworks system. + + Args: + service_provider_id (str): Service Provider/ Enterprise where group is hosted. + group_id (str): Group ID where target number for call flow is located. + number (str): Target number for call flow. + number_type (str): Type of number, options: DN, Extension, Alias + broadworks_entity_type (str): Broadworks entity type target number is associated with. + Options: Auto Attendant, Call Centre, Hunt Group, User + """ + + return reports.call_flow.main(self.api, service_provider_id, group_id, number, number_type, + broadworks_entity_type) + + \ No newline at end of file diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py new file mode 100644 index 0000000..f2ec1ef --- /dev/null +++ b/odins_spear/reports/call_flow.py @@ -0,0 +1,57 @@ +import json + +import odins_spear.logger as logger + +from .report_utils.graphviz_module import GraphvizModule + +def main(api, service_provider_id: str, group_id: str, number: str, number_type: str, + broadworks_entity_type: str): + + """ + methods needed: + - get.auto_attendants - X + - get.auto_attendant - X + + - get.group_call_centers - X + - get.group_call_center - X + - get.group_call_center_bounced_calls - X + - get.group_call_center_forced_forwarding - X + - get.group_call_center_overflow - X + - get.group_call_center_stranded_calls - X + - get.group_call_center_stranded_unavailable - X + + - get.group_hunt_groups - X + - get.group_hunt_group - X + + - get.users - X + - get.user_by_id - X + + - get.user_call_forwarding_always - X + - get.user_call_forwarding_busy - X + - get.user_call_forwarding_no_answer - X + - get.user_call_forwarding_not_reachable - X + + - get.group_schedules - X + - get.group_events - X + - get.user_alternate_numbers - X + """ + + # Gather entities + + # locate number using broadworks_entity_type to zone in on correct location + + # follow and map how the routing options. each routing instance will need to be followed. + + # create a graphviz chart + + # save the chart to local machine as svg. + + + graph = GraphvizModule( + "./os_reports/" + ) + graph.generate_call_flow_graph( + "flow", + "0" + ) + pass \ No newline at end of file diff --git a/odins_spear/reports/report_utils/graphviz_module.py b/odins_spear/reports/report_utils/graphviz_module.py new file mode 100644 index 0000000..5c92fcd --- /dev/null +++ b/odins_spear/reports/report_utils/graphviz_module.py @@ -0,0 +1,86 @@ +import graphviz + +import odins_spear.logger as logger + +class GraphvizModule: + + """ + Colours: + - Black: 020300 + - Orange: FFA400 - Darker: B87700 + - Red: 95190C - Darker: 5E1008 + - Purple: 5B5F97 - Darker: 3D4066 + - Redish: A23E48 - Darker: 68272E + """ + + NODE_STYLING = { + 'start' : { + 'shape': 'oval', + 'style': 'filled', + 'margin': '0.2', + 'color': '#020300', + 'fontname': 'Arial', + 'fontcolor': 'white' + }, + 'end': { + 'shape': 'box', + 'style': 'filled', + 'margin': '0.2', + 'color': '#020300', + 'fontname': 'Arial', + 'fontcolor': 'white' + }, + 'auto_attendant': { + 'shape': 'circle', + 'style': 'filled', + 'margin': '0.2', + 'color': '#5E1008', + 'fillcolor': '#FF0000', + 'fontname': 'Arial', + 'fontcolor': 'white' + }, + 'call_centre': { + 'shape': 'parallelogram', + 'style': 'filled', + 'margin': '0.2', + 'color': '#68272E', + 'fillcolor': '#FFC0CB', + 'fontname': 'Arial', + 'fontcolor': 'white' + }, + 'hunt_group': { + 'shape': 'parallelogram', + 'style': 'filled', + 'margin': '0.2', + 'color': '#3D4066', + 'fillcolor': '#9C1FE9', + 'fontname': 'Arial', + 'fontcolor': 'white' + }, + 'user': { + 'shape': 'diamond', + 'style': 'filled', + 'margin': '0.2', + 'color': '#B87700', + 'fillcolor': '#FBA200', + 'fontname': 'Arial', + 'fontcolor': 'white' + } + } + + def __init__(self, output_directory: str =None): + + self.dot = graphviz.Digraph() + self.output_directory = output_directory + + + def generate_call_flow_graph(self, flow: object, number: str): + self.dot.attr(name=f"call_flow_of_{number}", label=number, fontname='Arial', + fontcolor='white', rankdir="LR") + + + def _save_graph(self, filename: str): + self.dot.render(directory=self.output_directory, filename=filename, + format="svg", cleanup=True).replace('\\', '/') + + \ No newline at end of file diff --git a/odins_spear/reports/report_utils/parsing.py b/odins_spear/reports/report_utils/parsing.py new file mode 100644 index 0000000..cba18c9 --- /dev/null +++ b/odins_spear/reports/report_utils/parsing.py @@ -0,0 +1,9 @@ +""" Graphviz formatting for standardisation in reports. +""" +import odins_spear.logger as logger + +class CallFlowModule: + + def __init__(self): + pass + From e380383077471553b21f779681044871b51805b3 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Mon, 29 Apr 2024 17:01:34 +0100 Subject: [PATCH 02/25] Update broadwork_entities.py --- odins_spear/store/broadwork_entities.py | 247 ++++-------------------- 1 file changed, 42 insertions(+), 205 deletions(-) diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index 8836a70..b7e4e02 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -9,17 +9,6 @@ class ServiceProvider: name: str groups: List['Group'] = field(default_factory=list) is_enterprise: bool = False - default_domain: str = None - support_email: str = None - contact_name: str = None - contact_number: str = None - contact_email: str = None - address_line1: str = None - city: str = None - state_or_province: str = None - zip_or_postcode: str = None - country: str = None - use_service_provider_language: bool = False @dataclass(kw_only=True) @@ -28,22 +17,8 @@ class Group: id: str name: str default_domain: str - user_limit: int = None - user_count: int = None - calling_line_id_name: str = None calling_line_id_phone_number: str = None - calling_line_id_display_phone_number: str = None - time_zone: str = None - time_zone_display_name: str = None - location_dialing_code: str = None - contact_name: str = None - contact_number: str = None - contact_email: str = None - address_line1: str = None - address_line2: str = None - city: str = None - state_or_province: str = None - country: str = None + auto_attendants: List['AutoAttendant'] = field(default_factory=list) trunk_groups: List['TrunkGroup'] = field(default_factory=list) call_centers: List['CallCenter'] = field(default_factory=list) hunt_groups: List['HuntGroup'] = field(default_factory=list) @@ -57,55 +32,19 @@ def __post_init__(self): @dataclass(kw_only=True) class TrunkGroup: - name: str + service_user_id: str group: Type['Group'] - access_device: Type['Device'] users: List['User'] = field(default_factory=list) - allow_termination_to_dtg_identity: bool = False - allow_termination_to_trunk_group_identity: bool = False - allow_unscreened_calls: bool = False - allow_unscreened_emergency_calls: bool = False - capacity_exceeded_trap_initial_calls: int = None - capacity_exceeded_trap_offset_calls: int = None - clid_source_for_screened_calls_policy: str = None - continuous_options_sending_interval_seconds: int = None - enable_bursting: bool = False - enable_network_address_identity: bool = False - failure_options_sending_interval_seconds: int = None - failure_threshold_counter: int = None - include_dtg_identity: bool = False - include_otg_identity_for_network_calls: bool = False - include_trunk_group_identity: bool = False - include_trunk_group_identity_for_network_calls: bool = False - invitation_timeout: int = None - invite_failure_threshold_counter: int = None - invite_failure_threshold_window_seconds: int = None - pilot_user_call_optimization_policy: str = None - pilot_user_calling_line_asserted_identity_policy: str = None - pilot_user_calling_line_identity_for_emergency_calls_policy: str = None - pilot_user_calling_line_identity_for_external_calls_policy: str = None - pilot_user_charge_number_policy: str = None - prefix_enabled: bool = False - require_authentication: bool = False - route_to_peering_domain: bool = False - send_continuous_options_message: bool = False - stateful_rerouting_enabled: bool = False - success_threshold_counter: int = None - use_system_clid_source_for_screened_calls_policy: bool = False - use_system_calling_line_asserted_identity_policy: bool = False - use_system_user_lookup_policy: bool = False - user_lookup_policy: str = None max_active_calls: int = None - max_incoming_calls: int = None - max_outgoing_calls: int = None + bursting_max_active_calls: bool = False def __post_init__(self): - self.service_provider_id = self.group.service_provider_id.id + self.group.trunk_groups.append(self) @dataclass(kw_only=True) class AAKey: - key_number: int + number: int action: str description: str = None phone_number: str = None @@ -114,98 +53,78 @@ class AAKey: @dataclass(kw_only=True) class AAMenu: - announcement_selection: str = None enable_first_menu_level_extension_dialing: bool = False keys: List[AAKey] = field(default_factory=list) @dataclass(kw_only=True) class AutoAttendant: + service_user_id: str name: str group: Type['Group'] - calling_line_id_last_name: str = None - calling_line_id_first_name: str = None - hiragana_last_name: str = None - hiragana_first_name: str = None - language: str = None - time_zone: str = None - time_zone_display_name: str = None + extension: str = None + phone_number: str = None aliases: List[str] = field(default_factory= [list]) type: str = None - enable_video: bool = False - extension_dialing_scope: str = None - name_dialing_scope: str = None - name_dialing_entries: str = None business_hours_menu: Type['AAMenu'] = None after_hours_menu: Type['AAMenu'] = None - service_user_id: str = None + def __post_init__(self): - self.serviceProviderId = self.group.ServiceProvider.id + self.group.auto_attendants.append(self) @dataclass(kw_only=True) class CallCenter: - name: str + service_user_id: str group: Type['Group'] agents: List['User'] = field(default_factory=list) - enable_video: bool = False - allow_caller_to_dial_escape_digit: bool = False - reset_call_statistics_upon_entry_in_queue: bool = True - allow_agent_logoff: bool = True - allow_call_waiting_for_agents: bool = True - play_ringing_when_offering_call: bool = True - external_preferred_audio_codec: str = None - internal_preferred_audio_codec: str = None - enable_reporting: bool = False - allow_calls_to_agents_in_wrap_up: bool = True - override_agent_wrap_up_time: bool = False - enable_automatic_state_change_for_agents: bool = False - force_delivery_of_calls: bool = False + extension: str = None + phone_numner: str = None + name: str = None type: str = None - service_user_id_prefix: str = None - calling_line_id_last_name: str = None - calling_line_id_first_name: str = None - password: str = field(default_factory=gen.generate_password) policy: str = None - routing_type: str = None - queue_length: int = None - escape_digit: str = None - service_user_id: str = None - + + bounced_calls_enabled: bool = False + bounced_calls_transfer_to_phone_number: bool = False + overflow_calls_action: str = None + overflow_calls_transfer_to_phone_number: bool = False + stranded_calls_action: str = None + stranded_calls_transfer_to_phone_number: bool = False + stranded_call_unavailable_action: str = None + stranded_call_unavailable_transfer_to_phone_number: bool = False + + #NOTE: Not sure which forwarding this is. + forced_forwarding_enabled: bool = False + forced_forwarding_forward_to_phone_number: str = None + + night_service: str = None + holiday_service: str = None + def __post_init__(self): - self.service_provider_id = self.group.service_provider_id.id self.group.call_centers.append(self) @dataclass(kw_only=True) class HuntGroup: + service_user_id: str name: str group: Type['Group'] agents: List['User'] = field(default_factory=list) - calling_line_id_last_name: str = None - calling_line_id_first_name: str = None - hiragana_last_name: str = None - hiragana_first_name: str = None - language: str = None - time_zone: str = None aliases: List[str] = field(default_factory=list) + extension: str = None + phone_number: str = None policy: str = None - hunt_after_no_answer: bool = None - no_answer_number_of_rings: int = None - forward_after_timeout: bool = None + + forward_after_timeout_enabled: bool = False forward_timeout_seconds: int = None - allow_call_waiting_for_agents: bool = None - use_system_hunt_group_clid_setting: bool = None - include_hunt_group_name_in_clid: bool = None - enable_not_reachable_forwarding: bool = None - make_busy_when_not_reachable: bool = None - service_user_id: str = None - service_provider_id: str = None - group_id: str = None + no_answer_number_of_rings: int = None + no_answer_forward_to_phone_number: str = None + + call_forward_not_reachable_enabled: bool = False + call_forward_not_reachable_transfer_to_phone_number: str = None def __post_init__(self): - self.service_provider_id = self.group.service_provider_id.id self.group.hunt_groups.append(self) @@ -216,95 +135,13 @@ class User: first_name: str = None last_name: str = None extension: str = None - access_device_endpoint: Type['Device'] = None - calling_line_id_last_name: str = None - calling_line_id_first_name: str = None - hiragana_last_name: str = None - hiragana_first_name: str = None phone_number: str = None - calling_line_id_phone_number: str = None - password: str = None - department: Type['Department'] = None - department_full_path: str = None - language: str = None - time_zone: str = None - time_zone_display_name: str = None - default_alias: str = None - title: str = None - pager_phone_number: str = None - mobile_phone_number: str = None - email_address: str = None - yahoo_id: str = None - address_location: str = None - address: Type['Address'] = None - country_code: str = None - network_class_of_service: str = None - allow_video: bool = True - domain: str = None - endpoint_type: str = None aliases: List[str] = field(default_factory=list) - trunk_addressing: str = None - alternate_user_id: List[str] = field(default_factory=list) - is_enterprise: bool = False - password_expires_days: int = 2147483647 - service_provider_id: str = None - line_port: str = None + def __post_init__(self): - self.is_enterprise = self.group.service_provider.is_enterprise - self.service_provider_id = self.group.service_provider.id self.group.users.append(self) - self.id = self.id + self.group.default_domain - self.group_id = self.group.id - -@dataclass(kw_only=True) -class Device: - device_type: str - name: str - group: Type['Group'] - device_level: Type['Group'] - use_custom_user_name_password: bool = True - access_device_credential_name: str = None - access_device_credential_password: str = None - net_address: str = None - port: str = None - outbound_proxy_server_net_address: str = None - stun_server_net_address: str = None - mac_address: str = None - serial_number: str = None - description: str = None - physical_location: str = None - transport_protocol: str = None - profile: str = None - static_registration_capable: str = None - config_type: str = None - protocol_choice: List[str] = field(default_factory=list) - is_ip_address_optional: bool = True - use_domain: bool = True - is_mobility_manager_device: bool = False - device_configuration_option: str = None - static_line_ordering: bool = False - device_type_level: str = None - tags: List[str] = field(default_factory=list) - related_services: List[str] = field(default_factory=list) - protocol: str = None - user_name: str = None - group_id: int = field(init=False) - service_provider_id: int = field(init=False) - - # USER HAS THIS OVER DEVICE - # contacts - # support_visual_device_management - # number_of_ports - # number_of_assigned_ports - # status - # line_port - - def __post_init__(self): - self.group_id = self.group.id - self.service_provider_id = self.group.service_provider.id - @dataclass(kw_only=True) class Contact: From 0ba001e30fb0d30936d1e5c1315f77efe34934a8 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Tue, 30 Apr 2024 16:04:33 +0100 Subject: [PATCH 03/25] from_dict methods in-progress This is for when the JSON object comes from the API the developer can create an object from that JSON --- odins_spear/store/broadwork_entities.py | 58 ++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index b7e4e02..9771a63 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -1,7 +1,7 @@ -from dataclasses import dataclass, field, InitVar -from typing import List, Type +import json -from odins_spear.utils import generators as gen +from dataclasses import dataclass, field +from typing import List, Type @dataclass(kw_only=True) class ServiceProvider: @@ -9,7 +9,14 @@ class ServiceProvider: name: str groups: List['Group'] = field(default_factory=list) is_enterprise: bool = False - + + @classmethod + def from_dict(cls, data): + return cls( + id= data.get("serviceProviderId"), + name= data.get("serviceProviderId"), + is_enterprise= data.get("isEnterprise") + ) @dataclass(kw_only=True) class Group: @@ -28,7 +35,16 @@ def __post_init__(self): self.service_provider.groups.append(self) self.default_domain = '@' + self.default_domain - + + @classmethod + def from_dict(cls, service_provider: ServiceProvider, data): + return cls( + service_provider= service_provider, + id= data.get("groupId"), + name= data.get("groupName"), + default_domain= data.get("defaultDomain"), + calling_line_id_phone_number= data.get("callingLineIdPhoneNumber") + ) @dataclass(kw_only=True) class TrunkGroup: @@ -49,6 +65,16 @@ class AAKey: description: str = None phone_number: str = None submenu_id: int = None + + @classmethod + def from_dict(cls, data): + return cls( + number= data.get("key"), + action= data.get("action"), + description= data.get("description"), + phone_number= data.get("phoneNumber"), + submenu_id= data.get("submenuId") + ) @dataclass(kw_only=True) @@ -70,9 +96,27 @@ class AutoAttendant: after_hours_menu: Type['AAMenu'] = None - def __post_init__(self): - self.group.auto_attendants.append(self) + # def __post_init__(self): + # self.group.auto_attendants.append(self) + + @classmethod + def from_dict(cls, data): + return cls( + service_user_id=data.get("serviceUserId"), + name=data.get("serviceInstanceProfile").get("name"), + group=None, + extension=data.get("serviceInstanceProfile").get("extension"), + phone_number=data.get("serviceInstanceProfile").get("phoneNumber"), + aliases=data.get("serviceInstanceProfile").get("aliases"), + type=data.get("type"), + business_hours_menu=AAMenu( + enable_first_menu_level_extension_dialing=data.get('businessHoursMenu').get('enableFirstMenuLevelExtensionDialing'), + keys= [AAKey.from_dict(key) for key in data.get('businessHoursMenu').get('keys')]), + after_hours_menu=AAMenu( + enable_first_menu_level_extension_dialing=data.get('afterHoursMenu').get('enableFirstMenuLevelExtensionDialing'), + keys= [AAKey.from_dict(key) for key in data.get('afterHoursMenu').get('keys')]), + ) @dataclass(kw_only=True) class CallCenter: From e7766e6dcdc7b427d951504396249fc132108f35 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Tue, 30 Apr 2024 16:30:02 +0100 Subject: [PATCH 04/25] Continuing the from_dict methods Here I should have some examples for a perfect object for these methods. I should change the lists in the obkect to IF NONE = NONE. Instead of defaulting it to a list. Then in the from_dict I could set to None if doesnt exist. --- odins_spear/store/broadwork_entities.py | 51 ++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index 9771a63..841f226 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -52,10 +52,23 @@ class TrunkGroup: group: Type['Group'] users: List['User'] = field(default_factory=list) max_active_calls: int = None + bursting_enabled: bool = False bursting_max_active_calls: bool = False + pilot_user_id: str = None def __post_init__(self): self.group.trunk_groups.append(self) + + @classmethod + def from_dict(cls, group: Group, data): + return cls( + service_user_id= data.get(""), + group= group, + max_active_calls= data.get("maxActiveCalls"), + bursting_enabled= data.get("enableBursting") + bursting_max_active_calls= data.get("burstingMaxActiveCalls"), + pilot_user_id= data.get("pilotUserId") + ) @dataclass(kw_only=True) @@ -96,16 +109,16 @@ class AutoAttendant: after_hours_menu: Type['AAMenu'] = None - # def __post_init__(self): - # self.group.auto_attendants.append(self) + def __post_init__(self): + self.group.auto_attendants.append(self) @classmethod - def from_dict(cls, data): + def from_dict(cls, group: Group, data): return cls( service_user_id=data.get("serviceUserId"), name=data.get("serviceInstanceProfile").get("name"), - group=None, + group= group, extension=data.get("serviceInstanceProfile").get("extension"), phone_number=data.get("serviceInstanceProfile").get("phoneNumber"), aliases=data.get("serviceInstanceProfile").get("aliases"), @@ -124,7 +137,7 @@ class CallCenter: group: Type['Group'] agents: List['User'] = field(default_factory=list) extension: str = None - phone_numner: str = None + phone_number: str = None name: str = None type: str = None policy: str = None @@ -149,6 +162,34 @@ def __post_init__(self): self.group.call_centers.append(self) + @classmethod + def from_dict(cls, group: Group, data): + return cls( + service_user_id= data.get("serviceUserId"), + group= group, + extension= data.get("serviceInstanceProfile").get("extension"), + phone_number=data.get("serviceInstanceProfile").get("phoneNumber"), + name= data.get("serviceInstanceProfile").get("name") + type= data.get("type") + policy= data.get("policy") + + bounced_calls_enabled: bool = False + bounced_calls_transfer_to_phone_number: bool = False + overflow_calls_action: str = None + overflow_calls_transfer_to_phone_number: bool = False + stranded_calls_action: str = None + stranded_calls_transfer_to_phone_number: bool = False + stranded_call_unavailable_action: str = None + stranded_call_unavailable_transfer_to_phone_number: bool = False + + #NOTE: Not sure which forwarding this is. + forced_forwarding_enabled: bool = False + forced_forwarding_forward_to_phone_number: str = None + + night_service: str = None + holiday_service: str = None + ) + @dataclass(kw_only=True) class HuntGroup: service_user_id: str From aa51fda919b3c66b3e8e6feb2b1bc2096e225b3f Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Wed, 1 May 2024 13:28:56 +0100 Subject: [PATCH 05/25] Continued from_dict Need to confirm when this method will be used as the objects are conflicting such as a list of agents in the response back from hunt group --- odins_spear/store/broadwork_entities.py | 52 +++++++++++++++++-------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index 841f226..5e5ea39 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -65,7 +65,7 @@ def from_dict(cls, group: Group, data): service_user_id= data.get(""), group= group, max_active_calls= data.get("maxActiveCalls"), - bursting_enabled= data.get("enableBursting") + bursting_enabled= data.get("enableBursting"), bursting_max_active_calls= data.get("burstingMaxActiveCalls"), pilot_user_id= data.get("pilotUserId") ) @@ -169,25 +169,25 @@ def from_dict(cls, group: Group, data): group= group, extension= data.get("serviceInstanceProfile").get("extension"), phone_number=data.get("serviceInstanceProfile").get("phoneNumber"), - name= data.get("serviceInstanceProfile").get("name") - type= data.get("type") - policy= data.get("policy") + name= data.get("serviceInstanceProfile").get("name"), + type= data.get("type"), + policy= data.get("policy"), - bounced_calls_enabled: bool = False - bounced_calls_transfer_to_phone_number: bool = False - overflow_calls_action: str = None - overflow_calls_transfer_to_phone_number: bool = False - stranded_calls_action: str = None - stranded_calls_transfer_to_phone_number: bool = False - stranded_call_unavailable_action: str = None - stranded_call_unavailable_transfer_to_phone_number: bool = False + bounced_calls_enabled= data.get("bouncedCallsEnabled"), + bounced_calls_transfer_to_phone_number= data.get("bouncedCallsTransferToPhoneNumber"), + overflow_calls_action= data.get("overFlowCallsAction"), + overflow_calls_transfer_to_phone_number= data.get("overflowCallsTransferToPhoneNumber"), + stranded_calls_action= data.get("strandedCallsAction"), + stranded_calls_transfer_to_phone_number= data.get("strandedCallsTransferToPhoneNumber"), + stranded_call_unavailable_action= data.get("strandedCallUnavailableAction"), + stranded_call_unavailable_transfer_to_phone_number= data.get("strandedCallUnavailableTransferToPhoneNumber"), #NOTE: Not sure which forwarding this is. - forced_forwarding_enabled: bool = False - forced_forwarding_forward_to_phone_number: str = None + forced_forwarding_enabled= data.get("forcedForwardingEnabled"), + forced_forwarding_forward_to_phone_number= data.get("forcedForwardingEnabled"), - night_service: str = None - holiday_service: str = None + night_service= data.get("nightService"), + holiday_service= data.get("holidayService") ) @dataclass(kw_only=True) @@ -212,6 +212,26 @@ class HuntGroup: def __post_init__(self): self.group.hunt_groups.append(self) + @classmethod + def from_dict(cls, group: Group, agents: List["User"], data): + return cls( + service_user_id= data.get("serviceUserId"), + name= data.get("serviceInstanceProfile").get("name"), + group= group, + agents= agents, + aliases= data.get("serviceInstanceProfile").get("aliases"), + extension= data.get("serviceInstanceProfile").get("extension"), + phone_number= data.get("serviceInstanceProfile").get("phoneNumber"), + policy= data.get("policy"), + + forward_after_timeout_enabled= data.get("forwardAfterTimeoutEnabled"), + forward_timeout_seconds= data.get("forwardTimeoutSeconds"), + no_answer_number_of_rings= data.get("noAnswerNumberOfRings"), + no_answer_forward_to_phone_number= data.get("noAnswerForwardToPhoneNumber"), + + call_forward_not_reachable_enabled= data.get("callForwardNotReachableEnabled"), + call_forward_not_reachable_transfer_to_phone_number= data.get("callForwardNotReachableTransferToPhoneNumber") + ) @dataclass(kw_only=True) class User: From 033316172658b2ca5ef7a1f2abc9404f6343f45d Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Thu, 2 May 2024 18:23:59 +0100 Subject: [PATCH 06/25] Create __inti__.py --- odins_spear/reports/__inti__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 odins_spear/reports/__inti__.py diff --git a/odins_spear/reports/__inti__.py b/odins_spear/reports/__inti__.py new file mode 100644 index 0000000..16c4b6b --- /dev/null +++ b/odins_spear/reports/__inti__.py @@ -0,0 +1,5 @@ +__all__ = [ + "call_flow" +] + +from .call_flow import main From d468a3d607885fa2d736c70c73a72d336363eb4c Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Thu, 2 May 2024 18:39:21 +0100 Subject: [PATCH 07/25] rename --- odins_spear/reports/{__inti__.py => __init__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename odins_spear/reports/{__inti__.py => __init__.py} (100%) diff --git a/odins_spear/reports/__inti__.py b/odins_spear/reports/__init__.py similarity index 100% rename from odins_spear/reports/__inti__.py rename to odins_spear/reports/__init__.py From 8d7fce072092d2614bc5d5a73fb968abab247eb7 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Thu, 2 May 2024 18:39:39 +0100 Subject: [PATCH 08/25] added service provider singular --- odins_spear/methods/get.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/odins_spear/methods/get.py b/odins_spear/methods/get.py index 6de2ef8..7dcf2f9 100644 --- a/odins_spear/methods/get.py +++ b/odins_spear/methods/get.py @@ -553,6 +553,16 @@ def service_providers(self, reseller_id=None): return self.requester.get(endpoint) + def service_provider(self, service_provider_id: str): + """ + Args: + reseller_id (str): Only list the Service Provider IDs within the specified Reseller. + """ + + endpoint = f"/service-providers?serviceProviderId={service_provider_id}" + + return self.requester.get(endpoint) + # SERVICES def user_services_assigned(self, user_id: str): From 3f97868583f354726edcdde4b49837251e6413f8 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Thu, 2 May 2024 18:40:23 +0100 Subject: [PATCH 09/25] call_flow now pulling in data and saving to data_store --- odins_spear/reporter.py | 4 +-- odins_spear/reports/call_flow.py | 18 ++++++++-- odins_spear/reports/report_utils/parsing.py | 1 - odins_spear/store/broadwork_entities.py | 39 ++++++++++++++++++++- odins_spear/store/data_store.py | 34 ++++++------------ 5 files changed, 67 insertions(+), 29 deletions(-) diff --git a/odins_spear/reporter.py b/odins_spear/reporter.py index 3edb3c0..206a7a7 100644 --- a/odins_spear/reporter.py +++ b/odins_spear/reporter.py @@ -18,8 +18,8 @@ def call_flow(self, service_provider_id: str, group_id: str, number: str, number group_id (str): Group ID where target number for call flow is located. number (str): Target number for call flow. number_type (str): Type of number, options: DN, Extension, Alias - broadworks_entity_type (str): Broadworks entity type target number is associated with. - Options: Auto Attendant, Call Centre, Hunt Group, User + broadworks_entity_type (str): Broadworks entity type target number is associated with. \ + Options: Auto Attendant= AA, Call Centre= CC, Hunt Group= HG, User= U """ return reports.call_flow.main(self.api, service_provider_id, group_id, number, number_type, diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index f2ec1ef..4b10891 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -1,9 +1,10 @@ import json -import odins_spear.logger as logger - from .report_utils.graphviz_module import GraphvizModule +from odins_spear.store import DataStore +from odins_spear.store import broadwork_entities as bre + def main(api, service_provider_id: str, group_id: str, number: str, number_type: str, broadworks_entity_type: str): @@ -36,7 +37,20 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: - get.user_alternate_numbers - X """ + data_store = DataStore() + # Gather entities + service_provider = bre.ServiceProvider.from_dict(data=api.get.service_provider(service_provider_id)) + group = bre.Group.from_dict(service_provider=service_provider, data=api.get.group(service_provider_id, group_id)) + + data_store.store_objects(service_provider, group) + + auto_attendants = api.get.auto_attendants(service_provider_id, group_id) + for aa in auto_attendants: + auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) + data_store.store_objects(auto_attendant) + + # locate number using broadworks_entity_type to zone in on correct location diff --git a/odins_spear/reports/report_utils/parsing.py b/odins_spear/reports/report_utils/parsing.py index cba18c9..337f84d 100644 --- a/odins_spear/reports/report_utils/parsing.py +++ b/odins_spear/reports/report_utils/parsing.py @@ -1,6 +1,5 @@ """ Graphviz formatting for standardisation in reports. """ -import odins_spear.logger as logger class CallFlowModule: diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index 5e5ea39..11e0d46 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -247,6 +247,18 @@ class User: def __post_init__(self): self.group.users.append(self) + + @classmethod + def from_dict(cls, group: Group, data): + return cls( + group= group, + id= data.get("userId"), + first_name= data.get("firstName"), + last_name= data.get("lastName"), + extension= data.get("extension"), + phone_number= data.get("phoneNumber"), + aliases= data.get("aliases") + ) @dataclass(kw_only=True) class Contact: @@ -254,6 +266,13 @@ class Contact: number: str = None email: str = None + @classmethod + def from_dict(cls, data): + return cls( + name= data.get("name"), + number= data.get("number"), + email= data.get("email"), + ) @dataclass(kw_only=True) class Address: @@ -264,9 +283,27 @@ class Address: zip_or_postal_code: str country: str + @classmethod + def from_dict(cls, data): + return cls( + address_line1= data.get("addressLine1"), + address_line2= data.get("addressLine2"), + city= data.get("city"), + state_or_province= data.get("stateOrProvince"), + zip_or_postal_code= data.get("zipOrPostalCode"), + country= data.get("country") + ) @dataclass(kw_only=True) class Department: service_provider_id: str group_id: str - name: str \ No newline at end of file + name: str + + @classmethod + def from_dict(cls, data): + return cls( + service_provider_id= data.get("serviceProviderId"), + group_id= data.get("groupId"), + name= data.get("name") + ) \ No newline at end of file diff --git a/odins_spear/store/data_store.py b/odins_spear/store/data_store.py index 52a5a43..0fa24ca 100644 --- a/odins_spear/store/data_store.py +++ b/odins_spear/store/data_store.py @@ -12,30 +12,18 @@ class DataStore: Singleton design pattern to get store use get_instance(). """ - __instance = None - - @staticmethod - def get_instance(): - if DataStore.__instance is None: - DataStore() - return DataStore.__instance - def __init__(self): - if DataStore.__instance is not None: - raise Exception("Singleton cannot be instantiated more than once!") - else: - self.apis: List[api.Api] = [] - self.service_providers_enterprises: List[bre.ServiceProvider] = [] - self.groups: List[bre.Group] = [] - self.trunk_groups: List[bre.TrunkGroup] = [] - self.auto_attendants: List[bre.AutoAttendant] = [] - self.call_centers: List[bre.CallCenter] = [] - self.hunt_groups: List[bre.HuntGroup] = [] - self.users: List[bre.User] = [] - self.devices: List[bre.Device] = [] - self.other_entities = [] #Non-common or custom objects + self.apis: List[api.Api] = [] + self.service_providers_enterprises: List[bre.ServiceProvider] = [] + self.groups: List[bre.Group] = [] + self.trunk_groups: List[bre.TrunkGroup] = [] + self.auto_attendants: List[bre.AutoAttendant] = [] + self.call_centers: List[bre.CallCenter] = [] + self.hunt_groups: List[bre.HuntGroup] = [] + self.users: List[bre.User] = [] + self.devices: List[bre.Device] = [] + self.other_entities = [] #Non-common or custom objects - DataStore.__instance = self def get_group_state(api, group: bre.Group) -> None: """ takes in group id and loads group state into broadworks entities. @@ -45,7 +33,7 @@ def get_group_state(api, group: bre.Group) -> None: """ pass - def store_object(self, *entities) -> None: + def store_objects(self, *entities) -> None: """ Takes in objects within the odin_api and custom and stores in lists depending on type. From e614c6b98db26b2de7c90e068dade327d1ec6b09 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Thu, 2 May 2024 19:00:55 +0100 Subject: [PATCH 10/25] Up to call_flow storing hunt groups Hit the issue of agent in the dict but not made, when do I build each? either way there has to be some update. build hunt group then add in agents build users and then add to hunt groups --- odins_spear/reports/call_flow.py | 20 ++++++++++++++------ odins_spear/store/broadwork_entities.py | 2 ++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 4b10891..204101d 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -45,12 +45,20 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.store_objects(service_provider, group) - auto_attendants = api.get.auto_attendants(service_provider_id, group_id) - for aa in auto_attendants: - auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) - data_store.store_objects(auto_attendant) - - + # auto_attendants = api.get.auto_attendants(service_provider_id, group_id) + # for aa in auto_attendants: + # auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) + # data_store.auto_attendants.append(auto_attendant) + + # call_centers = api.get.group_call_centers(service_provider_id, group_id) + # for cc in call_centers: + # call_center = bre.CallCenter.from_dict(group=group, data= api.get.group_call_center(cc['serviceUserId'])) + # data_store.call_centers.append(call_center) + + hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) + for hg in hunt_groups: + hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) + data_store.hunt_groups.append(hunt_group) # locate number using broadworks_entity_type to zone in on correct location diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index 11e0d46..adbbd7d 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -138,6 +138,7 @@ class CallCenter: agents: List['User'] = field(default_factory=list) extension: str = None phone_number: str = None + aliases: List[str] = field(default_factory=list) name: str = None type: str = None policy: str = None @@ -170,6 +171,7 @@ def from_dict(cls, group: Group, data): extension= data.get("serviceInstanceProfile").get("extension"), phone_number=data.get("serviceInstanceProfile").get("phoneNumber"), name= data.get("serviceInstanceProfile").get("name"), + aliases= data.get("serviceInstanceProfile").get("aliases"), type= data.get("type"), policy= data.get("policy"), From 5c24b163befe14187f52f80662bd3c637e52335b Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Fri, 3 May 2024 14:20:44 +0100 Subject: [PATCH 11/25] users in cc, hg, and tg are now being pulled from user objects --- odins_spear/reports/call_flow.py | 5 +++++ odins_spear/store/broadwork_entities.py | 27 ++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 204101d..36a935d 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -50,6 +50,11 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: # auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) # data_store.auto_attendants.append(auto_attendant) + users = api.get.users(service_provider_id, group_id, extended=True) + for u in users: + user = bre.User.from_dict(group=group, data=u) + data_store.users.append(user) + # call_centers = api.get.group_call_centers(service_provider_id, group_id) # for cc in call_centers: # call_center = bre.CallCenter.from_dict(group=group, data= api.get.group_call_center(cc['serviceUserId'])) diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index adbbd7d..1a4c49b 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -61,9 +61,15 @@ def __post_init__(self): @classmethod def from_dict(cls, group: Group, data): + + # gather user IDs to gather user object + user_ids = [agent["userId"] for agent in data["agents"]] + users = _get_user_object_from_id(group, user_ids) + return cls( service_user_id= data.get(""), group= group, + users= users, max_active_calls= data.get("maxActiveCalls"), bursting_enabled= data.get("enableBursting"), bursting_max_active_calls= data.get("burstingMaxActiveCalls"), @@ -165,9 +171,15 @@ def __post_init__(self): @classmethod def from_dict(cls, group: Group, data): + + # gather user IDs to gather user object + agent_ids = [agent["userId"] for agent in data["agents"]] + agents = _get_user_object_from_id(group, agent_ids) + return cls( service_user_id= data.get("serviceUserId"), group= group, + agents= agents, extension= data.get("serviceInstanceProfile").get("extension"), phone_number=data.get("serviceInstanceProfile").get("phoneNumber"), name= data.get("serviceInstanceProfile").get("name"), @@ -215,12 +227,17 @@ def __post_init__(self): self.group.hunt_groups.append(self) @classmethod - def from_dict(cls, group: Group, agents: List["User"], data): + def from_dict(cls, group: Group, data): + + # gather user IDs to gather user object + agent_ids = [agent["userId"] for agent in data["agents"]] + agents = _get_user_object_from_id(group, agent_ids) + return cls( service_user_id= data.get("serviceUserId"), name= data.get("serviceInstanceProfile").get("name"), group= group, - agents= agents, + agents = agents, aliases= data.get("serviceInstanceProfile").get("aliases"), extension= data.get("serviceInstanceProfile").get("extension"), phone_number= data.get("serviceInstanceProfile").get("phoneNumber"), @@ -308,4 +325,8 @@ def from_dict(cls, data): service_provider_id= data.get("serviceProviderId"), group_id= data.get("groupId"), name= data.get("name") - ) \ No newline at end of file + ) + +def _get_user_object_from_id(group, user_ids: list): + return list(filter(lambda user: any(user_id in user.id for user_id in user_ids), + group.users)) \ No newline at end of file From a92986b1ecea9ab0d473d2a440a8dc8b1572aeb9 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Mon, 6 May 2024 14:53:46 +0100 Subject: [PATCH 12/25] update setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1e261a8..9bf8dae 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ description='Encapsulates the Odin API easing the use as well as additional features such as find alias', author='Jordan Prescott', author_email='jprescott23@gmail.com', - url='https://github.com/Jordan-Prescott/odin_api', + url='https://github.com/Jordan-Prescott/odins_spear', packages=find_packages(), install_requires=[ "certifi==2023.7.22", From a73dda7b9c53b0947d7163d4965df8fbbd5de588 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Wed, 8 May 2024 09:16:49 +0100 Subject: [PATCH 13/25] missing methods needed --- odins_spear/reports/call_flow.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 36a935d..6b1331d 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -15,11 +15,11 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: - get.group_call_centers - X - get.group_call_center - X - - get.group_call_center_bounced_calls - X - - get.group_call_center_forced_forwarding - X - - get.group_call_center_overflow - X - - get.group_call_center_stranded_calls - X - - get.group_call_center_stranded_unavailable - X + - get.group_call_center_bounced_calls - + - get.group_call_center_forced_forwarding - + - get.group_call_center_overflow - + - get.group_call_center_stranded_calls - + - get.group_call_center_stranded_unavailable - - get.group_hunt_groups - X - get.group_hunt_group - X @@ -27,10 +27,10 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: - get.users - X - get.user_by_id - X - - get.user_call_forwarding_always - X - - get.user_call_forwarding_busy - X - - get.user_call_forwarding_no_answer - X - - get.user_call_forwarding_not_reachable - X + - get.user_call_forwarding_always - + - get.user_call_forwarding_busy - + - get.user_call_forwarding_no_answer - + - get.user_call_forwarding_not_reachable - - get.group_schedules - X - get.group_events - X @@ -60,10 +60,16 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: # call_center = bre.CallCenter.from_dict(group=group, data= api.get.group_call_center(cc['serviceUserId'])) # data_store.call_centers.append(call_center) - hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) - for hg in hunt_groups: - hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) - data_store.hunt_groups.append(hunt_group) + # hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) + # for hg in hunt_groups: + # hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) + # data_store.hunt_groups.append(hunt_group) + + + + + + # locate number using broadworks_entity_type to zone in on correct location From 5ec03b2519b611c02229d7324a172f5fdbe12f96 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Fri, 10 May 2024 10:20:32 +0100 Subject: [PATCH 14/25] Remaining methods added --- odins_spear/methods/get.py | 153 ++++++++++++++++++++++++++++++++++++- 1 file changed, 150 insertions(+), 3 deletions(-) diff --git a/odins_spear/methods/get.py b/odins_spear/methods/get.py index 7dcf2f9..b8d254f 100644 --- a/odins_spear/methods/get.py +++ b/odins_spear/methods/get.py @@ -23,6 +23,22 @@ def session(self): # ADMINISTRATORS # ADVICE OF CHARGE # ALTERNATE NUMBERS + + def user_alternate_numbers(self, user_id: str): + """Fetches a list of a user/ service such as Auto Attendant, Hunt Group, or Call Centres + alternate numebrs. + + Args: + user_id (str): Target user/ service_user_id + + Returns: + Dict: List of all alternate numbers assigned to the user/ service. + """ + + endpoint = f"/users/alternate-numbers?userId={user_id}" + + return self.requester.get(endpoint) + # ANSWER CONFIRMATION # ALTERNATE USER ID # ANNOUNCEMENTS @@ -63,26 +79,142 @@ def group_call_centers(self, service_provider_id: str, group_id: str): return self.requester.get(endpoint) + def group_call_center(self, service_user_id: str): endpoint = f"/groups/call-centers?serviceUserId={service_user_id}" return self.requester.get(endpoint) + def user_call_center(self, user_id: str): endpoint = f"/users/call-center?userId={user_id}" return self.requester.get(endpoint) - + + + def group_call_center_bounced_calls(self, service_user_id: str): + + endpoint = f"/groups/call-centers/bounced-calls?serviceUserId={service_user_id}" + + return self.requester.get(endpoint) + + + def group_call_center_forced_forwarding(self, service_user_id: str): + + endpoint = f"/groups/call-centers/forced-forwarding?serviceUserId={service_user_id}" + + return self.requester.get(endpoint) + + + def group_call_center_overflow(self, service_user_id): + + endpoint = f"/groups/call-centers/overflow?serviceUserId={service_user_id}" + + return self.requester.get(endpoint) + + + def group_call_center_stranded_calls(self, service_user_id): + + endpoint = f"/groups/call-centers/stranded-calls?serviceUserId={service_user_id}" + + return self.requester.get(endpoint) + + + def group_call_center_stranded_calls_unavailable(self, service_user_id): + + endpoint = f"/groups/call-centers/stranded-calls-unavailable?serviceUserId={service_user_id}" + + return self.requester.get(endpoint) + # CALL CONTROL # CALL FORWARDING ALWAYS -# CALL FORWARDING ALWAYS SECONDARY + + def user_call_forwarding_always(self, user_id: str): + + endpoint = f"/users/call-forwarding-always?userId={user_id}" + + return self.requester.get(endpoint) + + + def bulk_call_forwarding_always(self, service_provider_id: str, group_id: str): + + endpoint = f"/users/call-forwarding-always/bulk?serviceProviderId={service_provider_id}&groupId={group_id}" + + return self.requester.get(endpoint) + + # CALL FORWARDING BUSY + + def user_call_forwarding_busy(self, user_id: str): + + endpoint = f"/users/call-forwarding-busy?userId={user_id}" + + return self.requester.get(endpoint) + + + def bulk_call_forwarding_busy(self, service_provider_id: str, group_id: str): + + endpoint = f"/users/call-forwarding-busy/bulk?serviceProviderId={service_provider_id}&groupId={group_id}" + + return self.requester.get(endpoint) + + # CALL FORWARDING NO ANSWER + + def user_call_forwarding_no_answer(self, user_id: str): + + endpoint = f"/users/call-forwarding-no-answer?userId={user_id}" + + return self.requester.get(endpoint) + + + def bulk_call_forwarding_no_answer(self, service_provider_id: str, group_id: str): + + endpoint = f"/users/call-forwarding-no-answer/bulk?serviceProviderId={service_provider_id}&groupId={group_id}" + + return self.requester.get(endpoint) + + # CALL FORWARDING NOT REACHABLE + + def user_call_forwarding_not_reachable(self, user_id: str): + + endpoint = f"/users/call-forwarding-not-reachable?userId={user_id}" + + return self.requester.get(endpoint) + + + def bulk_call_forwarding_not_reachable(self, service_provider_id: str, group_id: str): + + endpoint = f"/users/call-forwarding-not-reachable/bulk?serviceProviderId={service_provider_id}&groupId={group_id}" + + return self.requester.get(endpoint) + + # CALL FORWARDING SELECTIVE -# CALL FORWARDING SETTINGS + + def user_call_forwarding_selective(self, user_id: str): + + endpoint = f"/users/call-forwarding-selective?userId={user_id}" + + return self.requester.get(endpoint) + + + def user_call_forwarding_selective_criterias(self, user_id: str): + + endpoint = f"/users/call-forwarding-selective/criteria?userId={user_id}" + + return self.requester.get(endpoint) + + + def user_call_forwarding_selective_criteria(self, user_id: str, criteria_name: str): + + endpoint = f"/users/call-forwarding-selective/criteria?criteriaName={criteria_name}&userId={user_id}" + + return self.requester.get(endpoint) + # CALL NOTIFY # CALL PARK # CALL PICKUP @@ -533,6 +665,21 @@ def user_report(self, user_id: str): # ROUTE LIST # ROUTING PROFILE # SCHEDULES + + def group_schedules(self, service_provider_id: str, group_id: str): + + endpoint = f"/groups/schedules?serviceProviderId={service_provider_id}&groupId={group_id}" + + return self.requester.get(endpoint) + + + def group_events(self, service_provider_id: str, group_id: str, name: str, type: str): + + endpoint = f"/groups/events?serviceProviderId={service_provider_id}&groupId={group_id}&name={name}&type={type}" + + return self.requester.get(endpoint) + + # SECURITY CLASSIFICATION # SELECTIVE CALL ACCEPTANCE # SELECTIVE CALL REJECTION From 3a5538c9ef8cf4279a543f320a4ff9c604eaa888 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Fri, 10 May 2024 11:21:50 +0100 Subject: [PATCH 15/25] starting to locate number in entities --- odins_spear/reports/call_flow.py | 36 ++++++++++++++++---- odins_spear/reports/report_utils/__init__.py | 9 +++++ odins_spear/reports/report_utils/helpers.py | 13 +++++++ 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 odins_spear/reports/report_utils/__init__.py create mode 100644 odins_spear/reports/report_utils/helpers.py diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 6b1331d..c4c8558 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -1,6 +1,7 @@ import json from .report_utils.graphviz_module import GraphvizModule +from .report_utils.locaters import find_number from odins_spear.store import DataStore from odins_spear.store import broadwork_entities as bre @@ -52,8 +53,8 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: users = api.get.users(service_provider_id, group_id, extended=True) for u in users: - user = bre.User.from_dict(group=group, data=u) - data_store.users.append(user) + user = bre.User.from_dict(group=group, data=u) + data_store.users.append(user) # call_centers = api.get.group_call_centers(service_provider_id, group_id) # for cc in call_centers: @@ -64,14 +65,35 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: # for hg in hunt_groups: # hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) # data_store.hunt_groups.append(hunt_group) - - - - - + # locate number using broadworks_entity_type to zone in on correct location + if broadworks_entity_type == "user": + call_flow_start = find_number( + number, + number_type, + data_store.users + ) + if broadworks_entity_type == "auto attendant": + call_flow_start = find_number( + number, + number_type, + data_store.auto_attendants + ) + if broadworks_entity_type == "hunt group": + call_flow_start = find_number( + number, + number_type, + data_store.hunt_groups + ) + if broadworks_entity_type == "call center": + call_flow_start = find_number( + number, + number_type, + data_store.call_centers + ) + # follow and map how the routing options. each routing instance will need to be followed. diff --git a/odins_spear/reports/report_utils/__init__.py b/odins_spear/reports/report_utils/__init__.py new file mode 100644 index 0000000..f95106a --- /dev/null +++ b/odins_spear/reports/report_utils/__init__.py @@ -0,0 +1,9 @@ +__all__ = [ + "graphviz_module", + "locaters", + "parsing" +] + +from .graphviz_module import * +from .locaters import * +from .parsing import * diff --git a/odins_spear/reports/report_utils/helpers.py b/odins_spear/reports/report_utils/helpers.py new file mode 100644 index 0000000..7a78ac4 --- /dev/null +++ b/odins_spear/reports/report_utils/helpers.py @@ -0,0 +1,13 @@ +import re + +def find_number(number: str, number_type: str, broadwork_entities: list): + for entity in broadwork_entities: + if hasattr(entity, number_type) and getattr(entity, number_type) == number: + return entity + + if number_type == 'aliases': + for alias in entity.aliases: + if re.search(rf'\b{re.escape(alias)}\b', number): + return entity + + return None From 8490051d2275083f23194825d2a8b6287a6dd2e2 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Fri, 10 May 2024 11:55:11 +0100 Subject: [PATCH 16/25] function can now find number next: aliases and extension --- odins_spear/reports/call_flow.py | 7 +++---- odins_spear/reports/report_utils/__init__.py | 4 ++-- odins_spear/reports/report_utils/helpers.py | 15 ++++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index c4c8558..7adf360 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -1,7 +1,7 @@ import json from .report_utils.graphviz_module import GraphvizModule -from .report_utils.locaters import find_number +from .report_utils.helpers import find_number from odins_spear.store import DataStore from odins_spear.store import broadwork_entities as bre @@ -69,11 +69,10 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: # locate number using broadworks_entity_type to zone in on correct location - if broadworks_entity_type == "user": - call_flow_start = find_number( + call_flow_start = find_number( number, number_type, - data_store.users + getattr(data_store, broadworks_entity_type + "s") ) if broadworks_entity_type == "auto attendant": call_flow_start = find_number( diff --git a/odins_spear/reports/report_utils/__init__.py b/odins_spear/reports/report_utils/__init__.py index f95106a..041bccc 100644 --- a/odins_spear/reports/report_utils/__init__.py +++ b/odins_spear/reports/report_utils/__init__.py @@ -1,9 +1,9 @@ __all__ = [ "graphviz_module", - "locaters", + "helpers", "parsing" ] from .graphviz_module import * -from .locaters import * +from .helpers import * from .parsing import * diff --git a/odins_spear/reports/report_utils/helpers.py b/odins_spear/reports/report_utils/helpers.py index 7a78ac4..6376936 100644 --- a/odins_spear/reports/report_utils/helpers.py +++ b/odins_spear/reports/report_utils/helpers.py @@ -2,12 +2,13 @@ def find_number(number: str, number_type: str, broadwork_entities: list): for entity in broadwork_entities: - if hasattr(entity, number_type) and getattr(entity, number_type) == number: + if number_type == 'phone number' and number in entity.phone_number: return entity - - if number_type == 'aliases': - for alias in entity.aliases: - if re.search(rf'\b{re.escape(alias)}\b', number): - return entity - + elif number_type == 'extension' and number in entity.extension: + return entity + elif number_type == 'aliases': + for alias in entity.aliases: + if re.search(rf'\b{re.escape(alias)}\b', number): + return entity + return None From 587db80207b26b0a8aa2e8f128ac68bedcc53bc8 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Mon, 13 May 2024 12:49:50 +0100 Subject: [PATCH 17/25] mappings in datastore and pulling forwarding options --- odins_spear/reports/call_flow.py | 60 ++++++++++++------- odins_spear/reports/report_utils/__init__.py | 4 +- .../reports/report_utils/graphviz_module.py | 2 +- odins_spear/reports/report_utils/helpers.py | 19 +++++- odins_spear/reports/report_utils/parsing.py | 15 +++-- .../reports/report_utils/report_entities.py | 11 ++++ odins_spear/store/broadwork_entities.py | 7 ++- odins_spear/store/data_store.py | 45 +++++++++++++- 8 files changed, 130 insertions(+), 33 deletions(-) create mode 100644 odins_spear/reports/report_utils/report_entities.py diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 7adf360..1a4b546 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -1,7 +1,7 @@ import json from .report_utils.graphviz_module import GraphvizModule -from .report_utils.helpers import find_number +from .report_utils.helpers import find_entity_with_number_type from odins_spear.store import DataStore from odins_spear.store import broadwork_entities as bre @@ -52,9 +52,37 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: # data_store.auto_attendants.append(auto_attendant) users = api.get.users(service_provider_id, group_id, extended=True) + data_store.build_id_mapping() + + call_forward_always_users = [ + {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} + for item in api.get.bulk_call_forwarding_always(service_provider_id, group_id) if item["service"]["assigned"] + ] + + call_forward_busy_users = [ + {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} + for item in api.get.bulk_call_forwarding_busy(service_provider_id, group_id) if item["service"]["assigned"] + ] + + call_forward_no_answer_users = [ + {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} + for item in api.get.bulk_call_forwarding_no_answer(service_provider_id, group_id) if item["service"]["assigned"] + ] + + call_forward_not_reachable = [ + {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} + for item in api.get.bulk_call_forwarding_not_reachable(service_provider_id, group_id) if item["service"]["assigned"] + ] + for u in users: user = bre.User.from_dict(group=group, data=u) + + data_store.users.append(user) + + for u in call_forward_always_users: + if u["service"]["assigned"]: + pass # call_centers = api.get.group_call_centers(service_provider_id, group_id) # for cc in call_centers: @@ -66,33 +94,19 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: # hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) # data_store.hunt_groups.append(hunt_group) - - + # create mapping for easier searchability when gathering nodes + data_store.build_number_mapping() + # locate number using broadworks_entity_type to zone in on correct location - call_flow_start = find_number( + call_flow_start_node = find_entity_with_number_type( number, number_type, getattr(data_store, broadworks_entity_type + "s") ) - if broadworks_entity_type == "auto attendant": - call_flow_start = find_number( - number, - number_type, - data_store.auto_attendants - ) - if broadworks_entity_type == "hunt group": - call_flow_start = find_number( - number, - number_type, - data_store.hunt_groups - ) - if broadworks_entity_type == "call center": - call_flow_start = find_number( - number, - number_type, - data_store.call_centers - ) - + + # Nodes used in the graph + + # follow and map how the routing options. each routing instance will need to be followed. diff --git a/odins_spear/reports/report_utils/__init__.py b/odins_spear/reports/report_utils/__init__.py index 041bccc..46a9d3d 100644 --- a/odins_spear/reports/report_utils/__init__.py +++ b/odins_spear/reports/report_utils/__init__.py @@ -1,9 +1,11 @@ __all__ = [ "graphviz_module", "helpers", - "parsing" + "parsing", + "report_entities" ] from .graphviz_module import * from .helpers import * from .parsing import * +from .report_entities import * \ No newline at end of file diff --git a/odins_spear/reports/report_utils/graphviz_module.py b/odins_spear/reports/report_utils/graphviz_module.py index 5c92fcd..8e9a665 100644 --- a/odins_spear/reports/report_utils/graphviz_module.py +++ b/odins_spear/reports/report_utils/graphviz_module.py @@ -75,7 +75,7 @@ def __init__(self, output_directory: str =None): def generate_call_flow_graph(self, flow: object, number: str): - self.dot.attr(name=f"call_flow_of_{number}", label=number, fontname='Arial', + self.dot.attr(name=f"calls_to_{number}", label=number, fontname='Arial', fontcolor='white', rankdir="LR") diff --git a/odins_spear/reports/report_utils/helpers.py b/odins_spear/reports/report_utils/helpers.py index 6376936..98274e2 100644 --- a/odins_spear/reports/report_utils/helpers.py +++ b/odins_spear/reports/report_utils/helpers.py @@ -1,6 +1,19 @@ import re -def find_number(number: str, number_type: str, broadwork_entities: list): +def find_entity_with_number_type(number: str, number_type: str, broadwork_entities: list): + """ + Finds the phone number, extension, or alias in list of broadwork entities + and returns that entity once found. + + Args: + number (str): Target number looking for e.g. +1-123456789 or alias 0 + number_type (str): Number type of either phone number, extenion, or alias + broadwork_entities (list): List of broadwork entities number can be assigned to + + Returns: + object: Broadwork entity where number is assigned. + """ + for entity in broadwork_entities: if number_type == 'phone number' and number in entity.phone_number: return entity @@ -12,3 +25,7 @@ def find_number(number: str, number_type: str, broadwork_entities: list): return entity return None + + +def find_entity_without_number_type(number: str, data_store: object): + pass \ No newline at end of file diff --git a/odins_spear/reports/report_utils/parsing.py b/odins_spear/reports/report_utils/parsing.py index 337f84d..2c0661c 100644 --- a/odins_spear/reports/report_utils/parsing.py +++ b/odins_spear/reports/report_utils/parsing.py @@ -1,8 +1,13 @@ -""" Graphviz formatting for standardisation in reports. -""" +from odins_spear.store import broadwork_entities as bre -class CallFlowModule: +NODES = [] + +def call_flow_module(node: object, store: object): + start_node = node - def __init__(self): - pass + +#TODO: run over the entities and depening on what it is use the call forwards to pull the other entities +def traverse_entities(entity): + pass + diff --git a/odins_spear/reports/report_utils/report_entities.py b/odins_spear/reports/report_utils/report_entities.py new file mode 100644 index 0000000..d4c856b --- /dev/null +++ b/odins_spear/reports/report_utils/report_entities.py @@ -0,0 +1,11 @@ +class call_flow: + def __init__( + self, name, nodes: list) -> None: + """Usually a call flow to a number and how calls to this number flows through the system. + + :param name: Flow name. + :param nodes: List of the nodes that make up the flow. + """ + + self.name = name + self.nodes = nodes \ No newline at end of file diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index 1a4c49b..904a6f1 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -261,6 +261,11 @@ class User: extension: str = None phone_number: str = None aliases: List[str] = field(default_factory=list) + + call_forwarding_always: str = None + call_forwarding_busy: str = None + call_forwarding_no_answer: str = None + call_forwarding_not_reachable: str = None def __post_init__(self): @@ -326,7 +331,7 @@ def from_dict(cls, data): group_id= data.get("groupId"), name= data.get("name") ) - + def _get_user_object_from_id(group, user_ids: list): return list(filter(lambda user: any(user_id in user.id for user_id in user_ids), group.users)) \ No newline at end of file diff --git a/odins_spear/store/data_store.py b/odins_spear/store/data_store.py index 0fa24ca..cb3db59 100644 --- a/odins_spear/store/data_store.py +++ b/odins_spear/store/data_store.py @@ -24,7 +24,50 @@ def __init__(self): self.devices: List[bre.Device] = [] self.other_entities = [] #Non-common or custom objects - + + def build_id_mapping(self): + """ + Builds mapping of numbers IDs to entity. + + Example: {test@test.com: User, customID: CallCenter} + """ + + self.id_mapping = {} + + entities = self.auto_attendants + self.call_centers + \ + self.hunt_groups + self.users + + for e in entities: + try: + self.id_mapping[e.id] = e + except KeyError: + self.id_mapping[e.service_user_id] = e + + + def build_number_mapping(self): + """ + Builds mapping of numbers (phone numbers, extension, aliases) to entity. + + Example: {101: User, +1-123456789: CallCenter} + """ + import re + + self.number_mapping = {} + + entities = self.auto_attendants + self.call_centers + \ + self.hunt_groups + self.users + + for e in entities: + if e.phone_number: + self.number_mapping[e.phone_number] = e + if e.extension: + self.number_mapping[e.extension] = e + if e.aliases: + for a in e.aliases: + number = re.search(r'\d+', a).group() + self.number_mapping[number] = e + + def get_group_state(api, group: bre.Group) -> None: """ takes in group id and loads group state into broadworks entities. From 2f7282a44d4c4386e38468a2d918b6b86db247d5 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Mon, 13 May 2024 16:09:14 +0100 Subject: [PATCH 18/25] traversing entities Issue found - CC and HG data not complete ready for traversing need to add these in. --- odins_spear/reports/call_flow.py | 96 ++++++++++--------- .../reports/report_utils/graphviz_module.py | 10 +- odins_spear/reports/report_utils/helpers.py | 6 +- odins_spear/reports/report_utils/parsing.py | 50 ++++++++-- odins_spear/store/broadwork_entities.py | 7 +- odins_spear/store/data_store.py | 2 +- 6 files changed, 110 insertions(+), 61 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 1a4b546..f4f1b73 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -2,6 +2,8 @@ from .report_utils.graphviz_module import GraphvizModule from .report_utils.helpers import find_entity_with_number_type +from .report_utils.parsing import call_flow_module + from odins_spear.store import DataStore from odins_spear.store import broadwork_entities as bre @@ -28,10 +30,10 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: - get.users - X - get.user_by_id - X - - get.user_call_forwarding_always - - - get.user_call_forwarding_busy - - - get.user_call_forwarding_no_answer - - - get.user_call_forwarding_not_reachable - + - get.user_call_forwarding_always - X + - get.user_call_forwarding_busy - X + - get.user_call_forwarding_no_answer - X + - get.user_call_forwarding_not_reachable - X - get.group_schedules - X - get.group_events - X @@ -46,80 +48,82 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.store_objects(service_provider, group) - # auto_attendants = api.get.auto_attendants(service_provider_id, group_id) - # for aa in auto_attendants: - # auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) - # data_store.auto_attendants.append(auto_attendant) + auto_attendants = api.get.auto_attendants(service_provider_id, group_id) + for aa in auto_attendants: + auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) + data_store.auto_attendants.append(auto_attendant) users = api.get.users(service_provider_id, group_id, extended=True) data_store.build_id_mapping() + # Captures users with the forward fucntionality call_forward_always_users = [ - {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} - for item in api.get.bulk_call_forwarding_always(service_provider_id, group_id) if item["service"]["assigned"] + item["user"]["userId"] for item in api.get.bulk_call_forwarding_always(service_provider_id, group_id) if item["service"]["assigned"] \ + and item["data"]["isActive"] ] - + call_forward_busy_users = [ - {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} - for item in api.get.bulk_call_forwarding_busy(service_provider_id, group_id) if item["service"]["assigned"] + item["user"]["userId"] for item in api.get.bulk_call_forwarding_busy(service_provider_id, group_id) if item["service"]["assigned"] \ + and item["data"]["isActive"] ] call_forward_no_answer_users = [ - {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} - for item in api.get.bulk_call_forwarding_no_answer(service_provider_id, group_id) if item["service"]["assigned"] + item["user"]["userId"] for item in api.get.bulk_call_forwarding_no_answer(service_provider_id, group_id) if item["service"]["assigned"] \ + and item["data"]["isActive"] ] call_forward_not_reachable = [ - {"service_assigned": item["service"]["assigned"], "userId": item["user"]["userId"]} - for item in api.get.bulk_call_forwarding_not_reachable(service_provider_id, group_id) if item["service"]["assigned"] + item["user"]["userId"] for item in api.get.bulk_call_forwarding_not_reachable(service_provider_id, group_id) if item["service"]["assigned"] \ + and item["data"]["isActive"] ] - + for u in users: user = bre.User.from_dict(group=group, data=u) + if user.id in call_forward_always_users: + user.call_forwarding_always = str(api.get.user_call_forwarding_always(user.id)["forwardToPhoneNumber"]) + if user.id in call_forward_busy_users: + user.call_forwarding_busy = str(api.get.user_call_forwarding_busy(user.id)["forwardToPhoneNumber"]) + if user.id in call_forward_no_answer_users: + user.call_forwarding_no_answer = str(api.get.user_call_forwarding_no_answer(user.id)["forwardToPhoneNumber"]) + if user.id in call_forward_not_reachable: + user.call_forwarding_not_reachable = str(api.get.user_call_forwarding_no_answer(user.id)["forwardToPhoneNumber"]) data_store.users.append(user) + + + call_centers = api.get.group_call_centers(service_provider_id, group_id) + for cc in call_centers: + call_center = bre.CallCenter.from_dict(group= group, data= api.get.group_call_center(cc['serviceUserId'])) + + + - for u in call_forward_always_users: - if u["service"]["assigned"]: - pass - - # call_centers = api.get.group_call_centers(service_provider_id, group_id) - # for cc in call_centers: - # call_center = bre.CallCenter.from_dict(group=group, data= api.get.group_call_center(cc['serviceUserId'])) - # data_store.call_centers.append(call_center) - - # hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) - # for hg in hunt_groups: - # hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) - # data_store.hunt_groups.append(hunt_group) + + data_store.call_centers.append(call_center) + + hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) + for hg in hunt_groups: + hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) + data_store.hunt_groups.append(hunt_group) - # create mapping for easier searchability when gathering nodes - data_store.build_number_mapping() - # locate number using broadworks_entity_type to zone in on correct location call_flow_start_node = find_entity_with_number_type( number, number_type, getattr(data_store, broadworks_entity_type + "s") ) + call_flow_start_node._start_node = True # Nodes used in the graph - - + bre_nodes = call_flow_module(call_flow_start_node, data_store) # follow and map how the routing options. each routing instance will need to be followed. - - # create a graphviz chart - - # save the chart to local machine as svg. - - graph = GraphvizModule( - "./os_reports/" + "./os_reports/" ) graph.generate_call_flow_graph( - "flow", - "0" + bre_nodes, + number ) - pass \ No newline at end of file + graph._save_graph \ No newline at end of file diff --git a/odins_spear/reports/report_utils/graphviz_module.py b/odins_spear/reports/report_utils/graphviz_module.py index 8e9a665..9d7e895 100644 --- a/odins_spear/reports/report_utils/graphviz_module.py +++ b/odins_spear/reports/report_utils/graphviz_module.py @@ -74,7 +74,15 @@ def __init__(self, output_directory: str =None): self.output_directory = output_directory - def generate_call_flow_graph(self, flow: object, number: str): + def generate_call_flow_graph(self, nodes: list, number: str): + + + + + + + + self.dot.attr(name=f"calls_to_{number}", label=number, fontname='Arial', fontcolor='white', rankdir="LR") diff --git a/odins_spear/reports/report_utils/helpers.py b/odins_spear/reports/report_utils/helpers.py index 98274e2..d2ed0ab 100644 --- a/odins_spear/reports/report_utils/helpers.py +++ b/odins_spear/reports/report_utils/helpers.py @@ -24,8 +24,4 @@ def find_entity_with_number_type(number: str, number_type: str, broadwork_entiti if re.search(rf'\b{re.escape(alias)}\b', number): return entity - return None - - -def find_entity_without_number_type(number: str, data_store: object): - pass \ No newline at end of file + return None \ No newline at end of file diff --git a/odins_spear/reports/report_utils/parsing.py b/odins_spear/reports/report_utils/parsing.py index 2c0661c..162d04f 100644 --- a/odins_spear/reports/report_utils/parsing.py +++ b/odins_spear/reports/report_utils/parsing.py @@ -1,13 +1,51 @@ from odins_spear.store import broadwork_entities as bre +# nodes needed for graph generation NODES = [] +MAX_LEVEL = 30 -def call_flow_module(node: object, store: object): - start_node = node +def call_flow_module(node: object, data_store: object): + + # create mapping for easier searchability when gathering nodes + data_store.build_number_mapping() + + _traverse_connecting_entities(node, data_store) + + return NODES #TODO: run over the entities and depening on what it is use the call forwards to pull the other entities -def traverse_entities(entity): - pass - - +def _traverse_connecting_entities(entity: object, data_store: object): + if isinstance(entity, bre.User): + entity.call_forwarding_always = data_store.number_mapping.get(entity.call_forwarding_always) + if entity.call_forwarding_always: + _traverse_connecting_entities(entity.call_forwarding_always, data_store) + + entity.call_forwarding_busy = data_store.number_mapping.get(entity.call_forwarding_busy) + if entity.call_forwarding_busy: + _traverse_connecting_entities(entity.call_forwarding_busy, data_store) + + entity.call_forwarding_no_answer = data_store.number_mapping.get(entity.call_forwarding_no_answer) + if entity.call_forwarding_no_answer: + _traverse_connecting_entities(entity.call_forwarding_no_answer, data_store) + + entity.call_forwarding_not_reachable = data_store.number_mapping.get(entity.call_forwarding_not_reachable) + if entity.call_forwarding_not_reachable: + _traverse_connecting_entities(entity.call_forwarding_not_reachable, data_store) + + if isinstance(entity, bre.CallCenter): + if entity.bounced_calls_enabled: + entity.bounced_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.bounced_calls_transfer_to_phone_number) + if entity.bounced_calls_transfer_to_phone_number: + _traverse_connecting_entities(entity.bounced_calls_transfer_to_phone_number, data_store) + + overflow_calls_action: str = None + overflow_calls_transfer_to_phone_number: bool = False + + stranded_calls_action: str = None + stranded_calls_transfer_to_phone_number: bool = False + + stranded_call_unavailable_action: str = None + stranded_call_unavailable_transfer_to_phone_number: bool = False + + NODES.append(entity) # Append outside of the if condition diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index 904a6f1..e38a7a7 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -173,8 +173,11 @@ def __post_init__(self): def from_dict(cls, group: Group, data): # gather user IDs to gather user object - agent_ids = [agent["userId"] for agent in data["agents"]] - agents = _get_user_object_from_id(group, agent_ids) + try: + agent_ids = [agent["userId"] for agent in data["agents"]] + agents = _get_user_object_from_id(group, agent_ids) + except KeyError: + agents = [] return cls( service_user_id= data.get("serviceUserId"), diff --git a/odins_spear/store/data_store.py b/odins_spear/store/data_store.py index cb3db59..48bc73d 100644 --- a/odins_spear/store/data_store.py +++ b/odins_spear/store/data_store.py @@ -40,7 +40,7 @@ def build_id_mapping(self): for e in entities: try: self.id_mapping[e.id] = e - except KeyError: + except Exception: self.id_mapping[e.service_user_id] = e From 91d0838dce84607373250f6d374bf4c5f88953a4 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Mon, 13 May 2024 17:05:08 +0100 Subject: [PATCH 19/25] call center data now being captured --- odins_spear/reports/call_flow.py | 39 ++++++++++++++++++++++--- odins_spear/store/broadwork_entities.py | 2 -- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index f4f1b73..3f3c0de 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -20,7 +20,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: - get.group_call_center - X - get.group_call_center_bounced_calls - - get.group_call_center_forced_forwarding - - - get.group_call_center_overflow - + - get.group_overflow_settings - - get.group_call_center_stranded_calls - - get.group_call_center_stranded_unavailable - @@ -54,7 +54,6 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.auto_attendants.append(auto_attendant) users = api.get.users(service_provider_id, group_id, extended=True) - data_store.build_id_mapping() # Captures users with the forward fucntionality call_forward_always_users = [ @@ -96,9 +95,41 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: for cc in call_centers: call_center = bre.CallCenter.from_dict(group= group, data= api.get.group_call_center(cc['serviceUserId'])) + try: + overflow_settings = api.get.group_call_center_overflow(call_center.service_user_id) + call_center.overflow_calls_action = overflow_settings["action"] + call_center.overflow_calls_transfer_to_phone_number = overflow_settings["transferToPhoneNumner"] \ + if call_center.overflow_calls_action == "Transfer" else None + except Exception: + call_center.overflow_calls_action = None + call_center.overflow_calls_transfer_to_phone_number = None + try: + stranded_calls_settings = api.get.group_call_center_stranded_calls(call_center.service_user_id) + call_center.stranded_call_unavailable_action = stranded_calls_settings["action"] + call_center.stranded_call_unavailable_transfer_to_phone_number = stranded_calls_settings["transferPhoneNumber"] \ + if call_center.stranded_call_unavailable_action == "Transfer" else None + except Exception: + call_center.stranded_call_unavailable_action = None + call_center.stranded_call_unavailable_transfer_to_phone_number = None + + try: + stranded_calls_unavailable_settings = api.get.group_call_center_stranded_calls_unavailable(call_center.service_user_id) + call_center.stranded_call_unavailable_action = stranded_calls_unavailable_settings["action"] + call_center.stranded_call_unavailable_transfer_to_phone_number = stranded_calls_unavailable_settings["transferPhoneNumber"] \ + if call_center.stranded_call_unavailable_action == "Transfer" else None + except Exception: + call_center.stranded_call_unavailable_action = None + call_center.stranded_call_unavailable_transfer_to_phone_number = None - + try: + forced_forwarding_settings = api.get.group_call_center_forced_forwarding(call_center.service_user_id) + call_center.forced_forwarding_enabled = forced_forwarding_settings["enabled"] + call_center.forced_forwarding_forward_to_phone_number = forced_forwarding_settings["forwardToPhoneNumber"] \ + if call_center.forced_forwarding_enabled else None + except Exception: + call_center.forced_forwarding_enabled = False + call_center.forced_forwarding_forward_to_phone_number = None data_store.call_centers.append(call_center) @@ -118,7 +149,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: # Nodes used in the graph bre_nodes = call_flow_module(call_flow_start_node, data_store) - # follow and map how the routing options. each routing instance will need to be followed. + # build, generate, save graph graph = GraphvizModule( "./os_reports/" ) diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index e38a7a7..ca3533b 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -150,7 +150,6 @@ class CallCenter: policy: str = None bounced_calls_enabled: bool = False - bounced_calls_transfer_to_phone_number: bool = False overflow_calls_action: str = None overflow_calls_transfer_to_phone_number: bool = False stranded_calls_action: str = None @@ -191,7 +190,6 @@ def from_dict(cls, group: Group, data): policy= data.get("policy"), bounced_calls_enabled= data.get("bouncedCallsEnabled"), - bounced_calls_transfer_to_phone_number= data.get("bouncedCallsTransferToPhoneNumber"), overflow_calls_action= data.get("overFlowCallsAction"), overflow_calls_transfer_to_phone_number= data.get("overflowCallsTransferToPhoneNumber"), stranded_calls_action= data.get("strandedCallsAction"), From 5497ce10bdea65551418233afa38f76cff9b5e09 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Tue, 14 May 2024 16:30:13 +0100 Subject: [PATCH 20/25] Call center and users are being traversed Next is hunt groups and auto attendants --- odins_spear/reports/call_flow.py | 51 ++++------------ odins_spear/reports/report_utils/helpers.py | 2 +- odins_spear/reports/report_utils/parsing.py | 68 ++++++++++++--------- 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 3f3c0de..0f15621 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -11,35 +11,6 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: str, broadworks_entity_type: str): - """ - methods needed: - - get.auto_attendants - X - - get.auto_attendant - X - - - get.group_call_centers - X - - get.group_call_center - X - - get.group_call_center_bounced_calls - - - get.group_call_center_forced_forwarding - - - get.group_overflow_settings - - - get.group_call_center_stranded_calls - - - get.group_call_center_stranded_unavailable - - - - get.group_hunt_groups - X - - get.group_hunt_group - X - - - get.users - X - - get.user_by_id - X - - - get.user_call_forwarding_always - X - - get.user_call_forwarding_busy - X - - get.user_call_forwarding_no_answer - X - - get.user_call_forwarding_not_reachable - X - - - get.group_schedules - X - - get.group_events - X - - get.user_alternate_numbers - X - """ - data_store = DataStore() # Gather entities @@ -48,10 +19,10 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.store_objects(service_provider, group) - auto_attendants = api.get.auto_attendants(service_provider_id, group_id) - for aa in auto_attendants: - auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) - data_store.auto_attendants.append(auto_attendant) + # auto_attendants = api.get.auto_attendants(service_provider_id, group_id) + # for aa in auto_attendants: + # auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) + # data_store.auto_attendants.append(auto_attendant) users = api.get.users(service_provider_id, group_id, extended=True) @@ -133,15 +104,15 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.call_centers.append(call_center) - hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) - for hg in hunt_groups: - hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) - data_store.hunt_groups.append(hunt_group) + # hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) + # for hg in hunt_groups: + # hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) + # data_store.hunt_groups.append(hunt_group) # locate number using broadworks_entity_type to zone in on correct location call_flow_start_node = find_entity_with_number_type( number, - number_type, + number_type.lower(), getattr(data_store, broadworks_entity_type + "s") ) call_flow_start_node._start_node = True @@ -154,7 +125,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: "./os_reports/" ) graph.generate_call_flow_graph( - bre_nodes, - number + bre_nodes, + number ) graph._save_graph \ No newline at end of file diff --git a/odins_spear/reports/report_utils/helpers.py b/odins_spear/reports/report_utils/helpers.py index d2ed0ab..ed202c7 100644 --- a/odins_spear/reports/report_utils/helpers.py +++ b/odins_spear/reports/report_utils/helpers.py @@ -15,7 +15,7 @@ def find_entity_with_number_type(number: str, number_type: str, broadwork_entiti """ for entity in broadwork_entities: - if number_type == 'phone number' and number in entity.phone_number: + if number_type == 'dn' and number in entity.phone_number: return entity elif number_type == 'extension' and number in entity.extension: return entity diff --git a/odins_spear/reports/report_utils/parsing.py b/odins_spear/reports/report_utils/parsing.py index 162d04f..0e7793b 100644 --- a/odins_spear/reports/report_utils/parsing.py +++ b/odins_spear/reports/report_utils/parsing.py @@ -16,36 +16,44 @@ def call_flow_module(node: object, data_store: object): #TODO: run over the entities and depening on what it is use the call forwards to pull the other entities def _traverse_connecting_entities(entity: object, data_store: object): - if isinstance(entity, bre.User): - entity.call_forwarding_always = data_store.number_mapping.get(entity.call_forwarding_always) - if entity.call_forwarding_always: - _traverse_connecting_entities(entity.call_forwarding_always, data_store) + try: + if isinstance(entity, bre.User): + entity.call_forwarding_always = data_store.number_mapping.get(entity.call_forwarding_always) + if entity.call_forwarding_always: + _traverse_connecting_entities(entity.call_forwarding_always, data_store) + + entity.call_forwarding_busy = data_store.number_mapping.get(entity.call_forwarding_busy) + if entity.call_forwarding_busy: + _traverse_connecting_entities(entity.call_forwarding_busy, data_store) + + entity.call_forwarding_no_answer = data_store.number_mapping.get(entity.call_forwarding_no_answer) + if entity.call_forwarding_no_answer: + _traverse_connecting_entities(entity.call_forwarding_no_answer, data_store) + + entity.call_forwarding_not_reachable = data_store.number_mapping.get(entity.call_forwarding_not_reachable) + if entity.call_forwarding_not_reachable: + _traverse_connecting_entities(entity.call_forwarding_not_reachable, data_store) - entity.call_forwarding_busy = data_store.number_mapping.get(entity.call_forwarding_busy) - if entity.call_forwarding_busy: - _traverse_connecting_entities(entity.call_forwarding_busy, data_store) - - entity.call_forwarding_no_answer = data_store.number_mapping.get(entity.call_forwarding_no_answer) - if entity.call_forwarding_no_answer: - _traverse_connecting_entities(entity.call_forwarding_no_answer, data_store) - - entity.call_forwarding_not_reachable = data_store.number_mapping.get(entity.call_forwarding_not_reachable) - if entity.call_forwarding_not_reachable: - _traverse_connecting_entities(entity.call_forwarding_not_reachable, data_store) - - if isinstance(entity, bre.CallCenter): - if entity.bounced_calls_enabled: - entity.bounced_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.bounced_calls_transfer_to_phone_number) - if entity.bounced_calls_transfer_to_phone_number: - _traverse_connecting_entities(entity.bounced_calls_transfer_to_phone_number, data_store) - - overflow_calls_action: str = None - overflow_calls_transfer_to_phone_number: bool = False - - stranded_calls_action: str = None - stranded_calls_transfer_to_phone_number: bool = False + if isinstance(entity, bre.CallCenter): + if entity.bounced_calls_enabled: + entity.bounced_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.bounced_calls_transfer_to_phone_number) + if entity.bounced_calls_transfer_to_phone_number: + _traverse_connecting_entities(entity.bounced_calls_transfer_to_phone_number, data_store) + + if entity.overflow_calls_action: + entity.overflow_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.overflow_calls_transfer_to_phone_number) + if entity.overflow_calls_transfer_to_phone_number: + _traverse_connecting_entities(entity.overflow_calls_transfer_to_phone_number, data_store) + + if entity.stranded_calls_action: + entity.stranded_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.stranded_calls_transfer_to_phone_number) + if entity.stranded_calls_transfer_to_phone_number: + _traverse_connecting_entities(entity.stranded_calls_transfer_to_phone_number, data_store) - stranded_call_unavailable_action: str = None - stranded_call_unavailable_transfer_to_phone_number: bool = False - + if entity.stranded_call_unavailable_action: + entity.stranded_call_unavailable_transfer_to_phone_number = data_store.number_mapping.get(entity.stranded_call_unavailable_transfer_to_phone_number) + if entity.stranded_call_unavailable_transfer_to_phone_number: + _traverse_connecting_entities(entity.stranded_call_unavailable_transfer_to_phone_number, data_store) + except TypeError: + pass NODES.append(entity) # Append outside of the if condition From 38e12af781ca661e2f01ab0f437d024663b9a824 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Wed, 15 May 2024 14:48:13 +0100 Subject: [PATCH 21/25] Feature working as first draft Need to clean up calling the method --- odins_spear/reports/call_flow.py | 20 +-- .../reports/report_utils/graphviz_module.py | 73 +++++++-- odins_spear/reports/report_utils/parsing.py | 80 +++++++--- odins_spear/store/broadwork_entities.py | 8 +- os_reports/Calls To 3213061544.svg | 150 ++++++++++++++++++ 5 files changed, 285 insertions(+), 46 deletions(-) create mode 100644 os_reports/Calls To 3213061544.svg diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 0f15621..a91a594 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -19,10 +19,10 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.store_objects(service_provider, group) - # auto_attendants = api.get.auto_attendants(service_provider_id, group_id) - # for aa in auto_attendants: - # auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) - # data_store.auto_attendants.append(auto_attendant) + auto_attendants = api.get.auto_attendants(service_provider_id, group_id) + for aa in auto_attendants: + auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) + data_store.auto_attendants.append(auto_attendant) users = api.get.users(service_provider_id, group_id, extended=True) @@ -69,7 +69,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: try: overflow_settings = api.get.group_call_center_overflow(call_center.service_user_id) call_center.overflow_calls_action = overflow_settings["action"] - call_center.overflow_calls_transfer_to_phone_number = overflow_settings["transferToPhoneNumner"] \ + call_center.overflow_calls_transfer_to_phone_number = overflow_settings["transferPhoneNumber"] \ if call_center.overflow_calls_action == "Transfer" else None except Exception: call_center.overflow_calls_action = None @@ -104,10 +104,10 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.call_centers.append(call_center) - # hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) - # for hg in hunt_groups: - # hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) - # data_store.hunt_groups.append(hunt_group) + hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) + for hg in hunt_groups: + hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) + data_store.hunt_groups.append(hunt_group) # locate number using broadworks_entity_type to zone in on correct location call_flow_start_node = find_entity_with_number_type( @@ -128,4 +128,4 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: bre_nodes, number ) - graph._save_graph \ No newline at end of file + graph._save_graph(f"Calls To {number}") \ No newline at end of file diff --git a/odins_spear/reports/report_utils/graphviz_module.py b/odins_spear/reports/report_utils/graphviz_module.py index 9d7e895..8872ef1 100644 --- a/odins_spear/reports/report_utils/graphviz_module.py +++ b/odins_spear/reports/report_utils/graphviz_module.py @@ -1,6 +1,7 @@ import graphviz -import odins_spear.logger as logger +from odins_spear.store import broadwork_entities as bre + class GraphvizModule: @@ -75,18 +76,72 @@ def __init__(self, output_directory: str =None): def generate_call_flow_graph(self, nodes: list, number: str): - - - - - - - - self.dot.attr(name=f"calls_to_{number}", label=number, fontname='Arial', fontcolor='white', rankdir="LR") + # build nodes + self.dot.node("Start", "Start", GraphvizModule.NODE_STYLING["start"]) + for n in nodes: + if isinstance(n, bre.User): + self.dot.node(n.id, n.extension, GraphvizModule.NODE_STYLING["user"]) + elif isinstance(n, bre.CallCenter): + self.dot.node(n.service_user_id, n.extension, GraphvizModule.NODE_STYLING["call_centre"]) + elif isinstance(n, bre.HuntGroup): + self.dot.node(n.service_user_id, n.extension, GraphvizModule.NODE_STYLING["hunt_group"]) + elif isinstance(n, bre.AutoAttendant): + self.dot.node(n.service_user_id, n.extension, GraphvizModule.NODE_STYLING["auto_attendant"]) + + # build edges + for n in nodes: + try: + if n._start_node: + self.dot.edge("Start", n.id) + except AttributeError: + # node is note the start node + pass + + if isinstance(n, bre.User): + if n.call_forwarding_always: + self._format_edge(n, n.call_forwarding_always, "CFA") + if n.call_forwarding_busy: + self._format_edge(n, n.call_forwarding_busy, "CFB") + if n.call_forwarding_not_reachable: + self._format_edge(n, n.call_forwarding_not_reachable, "CFNR") + + elif isinstance(n, bre.CallCenter): + if n.bounced_calls_enabled: + self._format_edge(n, n.bounced_calls_transfer_to_phone_number, "BCT") + if n.overflow_calls_action: + self._format_edge(n, n.overflow_calls_transfer_to_phone_number, "OF") + if n.stranded_calls_action: + self._format_edge(n, n.stranded_calls_transfer_to_phone_number, "SCF") + if n.stranded_call_unavailable_action: + self._format_edge(n, n.stranded_call_unavailable_transfer_to_phone_number, "USCF") + + elif isinstance(n, bre.HuntGroup): + if n.forward_after_timeout_enabled: + self._format_edge(n, n.no_answer_forward_to_phone_number, "NACF") + if n.forward_after_timeout_enabled: + self._format_edge(n, n.call_forward_not_reachable_transfer_to_phone_number, "CFNR") + + elif isinstance(n, bre.AutoAttendant): + for key in n.business_hours_menu.keys: + if "Transfer" in key.action: + self._format_edge(n, key.phone_number, key.number) + def _format_edge(self, node_a: str, node_b: str, label: str): + try: + self.dot.edge(node_a.id, node_b.id, label) + except AttributeError: + try: + self.dot.edge(node_a.id, node_b.service_user_id, label) + except AttributeError: + try: + self.dot.edge(node_a.service_user_id, node_b.service_user_id, label) + except AttributeError: + self.dot.edge(node_a.service_user_id, node_b.id, label) + + def _save_graph(self, filename: str): self.dot.render(directory=self.output_directory, filename=filename, format="svg", cleanup=True).replace('\\', '/') diff --git a/odins_spear/reports/report_utils/parsing.py b/odins_spear/reports/report_utils/parsing.py index 0e7793b..bdac31c 100644 --- a/odins_spear/reports/report_utils/parsing.py +++ b/odins_spear/reports/report_utils/parsing.py @@ -9,51 +9,85 @@ def call_flow_module(node: object, data_store: object): # create mapping for easier searchability when gathering nodes data_store.build_number_mapping() + # traverses the enities that forward to eachother and adds all to NODES _traverse_connecting_entities(node, data_store) return NODES -#TODO: run over the entities and depening on what it is use the call forwards to pull the other entities -def _traverse_connecting_entities(entity: object, data_store: object): +def _traverse_connecting_entities(entity: object, data_store: object, visited= None): + if visited is None: + visited = [] + + if entity in visited: + return + else: + visited.append(entity) + try: if isinstance(entity, bre.User): entity.call_forwarding_always = data_store.number_mapping.get(entity.call_forwarding_always) - if entity.call_forwarding_always: - _traverse_connecting_entities(entity.call_forwarding_always, data_store) + if entity.call_forwarding_always and entity.call_forwarding_always not in visited: + _traverse_connecting_entities(entity.call_forwarding_always, data_store, visited) entity.call_forwarding_busy = data_store.number_mapping.get(entity.call_forwarding_busy) - if entity.call_forwarding_busy: - _traverse_connecting_entities(entity.call_forwarding_busy, data_store) + if entity.call_forwarding_busy and entity.call_forwarding_busy not in visited: + _traverse_connecting_entities(entity.call_forwarding_busy, data_store, visited) entity.call_forwarding_no_answer = data_store.number_mapping.get(entity.call_forwarding_no_answer) - if entity.call_forwarding_no_answer: - _traverse_connecting_entities(entity.call_forwarding_no_answer, data_store) + if entity.call_forwarding_no_answer and entity.call_forwarding_no_answer not in visited: + _traverse_connecting_entities(entity.call_forwarding_no_answer, data_store, visited) entity.call_forwarding_not_reachable = data_store.number_mapping.get(entity.call_forwarding_not_reachable) - if entity.call_forwarding_not_reachable: - _traverse_connecting_entities(entity.call_forwarding_not_reachable, data_store) + if entity.call_forwarding_not_reachable and entity.call_forwarding_not_reachable not in visited: + _traverse_connecting_entities(entity.call_forwarding_not_reachable, data_store, visited) if isinstance(entity, bre.CallCenter): if entity.bounced_calls_enabled: - entity.bounced_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.bounced_calls_transfer_to_phone_number) - if entity.bounced_calls_transfer_to_phone_number: - _traverse_connecting_entities(entity.bounced_calls_transfer_to_phone_number, data_store) + entity.bounced_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.bounced_calls_transfer_to_phone_number)) + if entity.bounced_calls_transfer_to_phone_number and entity.bounced_calls_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.bounced_calls_transfer_to_phone_number, data_store, visited) if entity.overflow_calls_action: - entity.overflow_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.overflow_calls_transfer_to_phone_number) - if entity.overflow_calls_transfer_to_phone_number: - _traverse_connecting_entities(entity.overflow_calls_transfer_to_phone_number, data_store) + entity.overflow_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.overflow_calls_transfer_to_phone_number)) + if entity.overflow_calls_transfer_to_phone_number and entity.overflow_calls_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.overflow_calls_transfer_to_phone_number, data_store, visited) if entity.stranded_calls_action: - entity.stranded_calls_transfer_to_phone_number = data_store.number_mapping.get(entity.stranded_calls_transfer_to_phone_number) - if entity.stranded_calls_transfer_to_phone_number: - _traverse_connecting_entities(entity.stranded_calls_transfer_to_phone_number, data_store) + entity.stranded_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.stranded_calls_transfer_to_phone_number)) + if entity.stranded_calls_transfer_to_phone_number and entity.stranded_calls_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.stranded_calls_transfer_to_phone_number, data_store, visited) if entity.stranded_call_unavailable_action: - entity.stranded_call_unavailable_transfer_to_phone_number = data_store.number_mapping.get(entity.stranded_call_unavailable_transfer_to_phone_number) - if entity.stranded_call_unavailable_transfer_to_phone_number: - _traverse_connecting_entities(entity.stranded_call_unavailable_transfer_to_phone_number, data_store) + entity.stranded_call_unavailable_transfer_to_phone_number = data_store.number_mapping.get(str(entity.stranded_call_unavailable_transfer_to_phone_number)) + if entity.stranded_call_unavailable_transfer_to_phone_number and entity.stranded_call_unavailable_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.stranded_call_unavailable_transfer_to_phone_number, data_store, visited) + + if isinstance(entity, bre.HuntGroup): + if entity.forward_after_timeout_enabled: + entity.no_answer_forward_to_phone_number = data_store.number_mapping.get(str(entity.no_answer_forward_to_phone_number)) + if entity.no_answer_forward_to_phone_number and entity.no_answer_forward_to_phone_number not in visited: + _traverse_connecting_entities(entity.no_answer_forward_to_phone_number, data_store, visited) + + if entity.call_forward_not_reachable_enabled: + entity.call_forward_not_reachable_transfer_to_phone_number = data_store.number_mapping.get(str(entity.call_forward_not_reachable_transfer_to_phone_number)) + if entity.call_forward_not_reachable_transfer_to_phone_number and entity.call_forward_not_reachable_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.call_forward_not_reachable_transfer_to_phone_number, data_store, visited) + + if isinstance(entity, bre.AutoAttendant): + for key in entity.business_hours_menu.keys: + if "Transfer" in key.action: + key.phone_number = data_store.number_mapping.get(str(key.phone_number)) + if key.phone_number and key.phone_number not in visited: + _traverse_connecting_entities(key.phone_number, data_store, visited) + + for key in entity.after_hours_menu.keys: + if "Transfer" in key.action: + key.phone_number = data_store.number_mapping.get(str(key.phone_number)) + if key.phone_number and key.phone_number not in visited: + _traverse_connecting_entities(key.phone_number, data_store, visited) except TypeError: - pass + # this object is further up the tree and a loop has occured - Move one and come back + pass + NODES.append(entity) # Append outside of the if condition diff --git a/odins_spear/store/broadwork_entities.py b/odins_spear/store/broadwork_entities.py index ca3533b..47f6837 100644 --- a/odins_spear/store/broadwork_entities.py +++ b/odins_spear/store/broadwork_entities.py @@ -244,13 +244,13 @@ def from_dict(cls, group: Group, data): phone_number= data.get("serviceInstanceProfile").get("phoneNumber"), policy= data.get("policy"), - forward_after_timeout_enabled= data.get("forwardAfterTimeoutEnabled"), + forward_after_timeout_enabled= data.get("forwardAfterTimeout"), forward_timeout_seconds= data.get("forwardTimeoutSeconds"), no_answer_number_of_rings= data.get("noAnswerNumberOfRings"), - no_answer_forward_to_phone_number= data.get("noAnswerForwardToPhoneNumber"), + no_answer_forward_to_phone_number= data.get("forwardToPhoneNumber"), - call_forward_not_reachable_enabled= data.get("callForwardNotReachableEnabled"), - call_forward_not_reachable_transfer_to_phone_number= data.get("callForwardNotReachableTransferToPhoneNumber") + call_forward_not_reachable_enabled= data.get("enableNotReachableForwarding"), + call_forward_not_reachable_transfer_to_phone_number= data.get("notReachableForwardToPhoneNumber") ) @dataclass(kw_only=True) diff --git a/os_reports/Calls To 3213061544.svg b/os_reports/Calls To 3213061544.svg new file mode 100644 index 0000000..fc5cec2 --- /dev/null +++ b/os_reports/Calls To 3213061544.svg @@ -0,0 +1,150 @@ + + + + + + + +3213061544 + + +Start + +Start + + + +JRDNEVA101@jrdneva.ev.com + +101 + + + +Start->JRDNEVA101@jrdneva.ev.com + + + + + +basic_aa@jrdneva.ev.com + +3001 + + + +basic_aa@jrdneva.ev.com->JRDNEVA101@jrdneva.ev.com + + +0 + + + +testing@jrdneva.ev.com + +2001 + + + +JRDNEVA102@jrdneva.ev.com + +102 + + + +testing@jrdneva.ev.com->JRDNEVA102@jrdneva.ev.com + + +CFNR + + + +testing@jrdneva.ev.com->JRDNEVA101@jrdneva.ev.com + + +NACF + + + +JRDNEVA104@jrdneva.ev.com + +104 + + + +JRDNEVA104@jrdneva.ev.com->basic_aa@jrdneva.ev.com + + +CFA + + + +JRDNEVA104@jrdneva.ev.com->testing@jrdneva.ev.com + + +CFB + + + +JRDNEVA141401_EVA_EL@jrdneva.ev.com + +141401 + + + +basic_cc@jrdneva.ev.com + +1001 + + + +basic_cc@jrdneva.ev.com->JRDNEVA141401_EVA_EL@jrdneva.ev.com + + +OF + + + +JRDNEVA102@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com + + +CFA + + + +JRDNEVA102@jrdneva.ev.com->basic_cc@jrdneva.ev.com + + +CFB + + + +JRDNEVA103@jrdneva.ev.com + +103 + + + +JRDNEVA103@jrdneva.ev.com->basic_cc@jrdneva.ev.com + + +CFB + + + +JRDNEVA101@jrdneva.ev.com->JRDNEVA102@jrdneva.ev.com + + +CFB + + + +JRDNEVA101@jrdneva.ev.com->JRDNEVA103@jrdneva.ev.com + + +CFNR + + + From bc5befe53988fc81b1657e7182568430070361a2 Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Wed, 15 May 2024 16:11:20 +0100 Subject: [PATCH 22/25] Made start easier --- odins_spear/reports/call_flow.py | 18 +++++++++++++----- odins_spear/reports/report_utils/helpers.py | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index a91a594..60003fd 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -1,5 +1,7 @@ import json +from tqdm import tqdm + from .report_utils.graphviz_module import GraphvizModule from .report_utils.helpers import find_entity_with_number_type from .report_utils.parsing import call_flow_module @@ -13,6 +15,8 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store = DataStore() + print("Start.\n") + print("Fetching Service Provider & Group details.") # Gather entities service_provider = bre.ServiceProvider.from_dict(data=api.get.service_provider(service_provider_id)) group = bre.Group.from_dict(service_provider=service_provider, data=api.get.group(service_provider_id, group_id)) @@ -20,7 +24,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.store_objects(service_provider, group) auto_attendants = api.get.auto_attendants(service_provider_id, group_id) - for aa in auto_attendants: + for aa in tqdm(auto_attendants, desc=f"Fetching all Auto Attendants."): auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) data_store.auto_attendants.append(auto_attendant) @@ -47,7 +51,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: and item["data"]["isActive"] ] - for u in users: + for u in tqdm(users, desc=f"Fetching all Users."): user = bre.User.from_dict(group=group, data=u) if user.id in call_forward_always_users: @@ -63,7 +67,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: call_centers = api.get.group_call_centers(service_provider_id, group_id) - for cc in call_centers: + for cc in tqdm(call_centers, desc=f"Fetching all Call Centers."): call_center = bre.CallCenter.from_dict(group= group, data= api.get.group_call_center(cc['serviceUserId'])) try: @@ -105,7 +109,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: data_store.call_centers.append(call_center) hunt_groups = api.get.group_hunt_groups(service_provider_id, group_id) - for hg in hunt_groups: + for hg in tqdm(hunt_groups, desc=f"Fetching all Hunt Groups."): hunt_group = bre.HuntGroup.from_dict(group=group, data= api.get.group_hunt_group(hg['serviceUserId'])) data_store.hunt_groups.append(hunt_group) @@ -118,8 +122,10 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: call_flow_start_node._start_node = True # Nodes used in the graph + print("Gathering nodes in flow.") bre_nodes = call_flow_module(call_flow_start_node, data_store) + print("Generating report.") # build, generate, save graph graph = GraphvizModule( "./os_reports/" @@ -128,4 +134,6 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: bre_nodes, number ) - graph._save_graph(f"Calls To {number}") \ No newline at end of file + print("Saving report.") + graph._save_graph(f"Calls To {number}") + print("\nEnd.") \ No newline at end of file diff --git a/odins_spear/reports/report_utils/helpers.py b/odins_spear/reports/report_utils/helpers.py index ed202c7..0787a19 100644 --- a/odins_spear/reports/report_utils/helpers.py +++ b/odins_spear/reports/report_utils/helpers.py @@ -19,7 +19,7 @@ def find_entity_with_number_type(number: str, number_type: str, broadwork_entiti return entity elif number_type == 'extension' and number in entity.extension: return entity - elif number_type == 'aliases': + elif number_type == 'alias': for alias in entity.aliases: if re.search(rf'\b{re.escape(alias)}\b', number): return entity From d07c998ea79f23783b6c6dde026778c380132ad2 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Thu, 16 May 2024 15:34:01 +0100 Subject: [PATCH 23/25] Solution can now handle numbers that leave the system Next I need to pull in the call forwards for when hg have services assigned to them. --- .gitignore | 2 +- odins_spear/reporter.py | 4 +- odins_spear/reports/call_flow.py | 5 +- .../reports/report_utils/graphviz_module.py | 40 ++-- odins_spear/reports/report_utils/parsing.py | 111 ++++++---- .../reports/report_utils/report_entities.py | 17 +- os_reports/Calls To 3213061544.svg | 197 ++++++++---------- 7 files changed, 197 insertions(+), 179 deletions(-) diff --git a/.gitignore b/.gitignore index b812ddc..e2513ca 100644 --- a/.gitignore +++ b/.gitignore @@ -162,5 +162,5 @@ sandbox.py # mac *.DS_Store - +os_reports/ call_flow_data-temp/ \ No newline at end of file diff --git a/odins_spear/reporter.py b/odins_spear/reporter.py index 206a7a7..7c2f44e 100644 --- a/odins_spear/reporter.py +++ b/odins_spear/reporter.py @@ -17,9 +17,9 @@ def call_flow(self, service_provider_id: str, group_id: str, number: str, number service_provider_id (str): Service Provider/ Enterprise where group is hosted. group_id (str): Group ID where target number for call flow is located. number (str): Target number for call flow. - number_type (str): Type of number, options: DN, Extension, Alias + number_type (str): Type of number, options: "dn": Direct Number, "extension": Extension, "alias": Alias broadworks_entity_type (str): Broadworks entity type target number is associated with. \ - Options: Auto Attendant= AA, Call Centre= CC, Hunt Group= HG, User= U + Options: "auto_attendant": Auto Attendant, "call_center": Call Center, "hunt_group": Hunt Group, "user": User """ return reports.call_flow.main(self.api, service_provider_id, group_id, number, number_type, diff --git a/odins_spear/reports/call_flow.py b/odins_spear/reports/call_flow.py index 60003fd..d82978e 100644 --- a/odins_spear/reports/call_flow.py +++ b/odins_spear/reports/call_flow.py @@ -28,6 +28,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: auto_attendant = bre.AutoAttendant.from_dict(group=group, data=api.get.auto_attendant(aa['serviceUserId'])) data_store.auto_attendants.append(auto_attendant) + print("Fetching all users this may take a couple of minutes, please wait.") users = api.get.users(service_provider_id, group_id, extended=True) # Captures users with the forward fucntionality @@ -51,7 +52,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: and item["data"]["isActive"] ] - for u in tqdm(users, desc=f"Fetching all Users."): + for u in tqdm(users, desc=f"Parsing all Users."): user = bre.User.from_dict(group=group, data=u) if user.id in call_forward_always_users: @@ -61,7 +62,7 @@ def main(api, service_provider_id: str, group_id: str, number: str, number_type: if user.id in call_forward_no_answer_users: user.call_forwarding_no_answer = str(api.get.user_call_forwarding_no_answer(user.id)["forwardToPhoneNumber"]) if user.id in call_forward_not_reachable: - user.call_forwarding_not_reachable = str(api.get.user_call_forwarding_no_answer(user.id)["forwardToPhoneNumber"]) + user.call_forwarding_not_reachable = str(api.get.user_call_forwarding_not_reachable(user.id)["forwardToPhoneNumber"]) data_store.users.append(user) diff --git a/odins_spear/reports/report_utils/graphviz_module.py b/odins_spear/reports/report_utils/graphviz_module.py index 8872ef1..ab1f8d1 100644 --- a/odins_spear/reports/report_utils/graphviz_module.py +++ b/odins_spear/reports/report_utils/graphviz_module.py @@ -1,7 +1,7 @@ import graphviz from odins_spear.store import broadwork_entities as bre - +from .report_entities import external_number class GraphvizModule: @@ -16,14 +16,14 @@ class GraphvizModule: NODE_STYLING = { 'start' : { - 'shape': 'oval', + 'shape': 'record', 'style': 'filled', 'margin': '0.2', 'color': '#020300', 'fontname': 'Arial', 'fontcolor': 'white' }, - 'end': { + 'exit': { 'shape': 'box', 'style': 'filled', 'margin': '0.2', @@ -41,7 +41,7 @@ class GraphvizModule: 'fontcolor': 'white' }, 'call_centre': { - 'shape': 'parallelogram', + 'shape': 'Mrecord', 'style': 'filled', 'margin': '0.2', 'color': '#68272E', @@ -50,7 +50,7 @@ class GraphvizModule: 'fontcolor': 'white' }, 'hunt_group': { - 'shape': 'parallelogram', + 'shape': 'Mrecord', 'style': 'filled', 'margin': '0.2', 'color': '#3D4066', @@ -59,16 +59,20 @@ class GraphvizModule: 'fontcolor': 'white' }, 'user': { - 'shape': 'diamond', + 'shape': 'Mrecord', 'style': 'filled', 'margin': '0.2', 'color': '#B87700', 'fillcolor': '#FBA200', 'fontname': 'Arial', 'fontcolor': 'white' - } + }, + } - + EDGE_STYLYING = { + "fontname": "Arial" + } + def __init__(self, output_directory: str =None): self.dot = graphviz.Digraph() @@ -90,12 +94,17 @@ def generate_call_flow_graph(self, nodes: list, number: str): self.dot.node(n.service_user_id, n.extension, GraphvizModule.NODE_STYLING["hunt_group"]) elif isinstance(n, bre.AutoAttendant): self.dot.node(n.service_user_id, n.extension, GraphvizModule.NODE_STYLING["auto_attendant"]) - + elif isinstance(n, external_number): + self.dot.node(n.id, n.id, GraphvizModule.NODE_STYLING["exit"]) + # build edges for n in nodes: try: if n._start_node: - self.dot.edge("Start", n.id) + try: + self.dot.edge("Start", n.id) + except Exception: + self.dot.edge("Start", n.service_user_id) except AttributeError: # node is note the start node pass @@ -121,25 +130,26 @@ def generate_call_flow_graph(self, nodes: list, number: str): elif isinstance(n, bre.HuntGroup): if n.forward_after_timeout_enabled: self._format_edge(n, n.no_answer_forward_to_phone_number, "NACF") - if n.forward_after_timeout_enabled: + if n.call_forward_not_reachable_enabled: self._format_edge(n, n.call_forward_not_reachable_transfer_to_phone_number, "CFNR") elif isinstance(n, bre.AutoAttendant): for key in n.business_hours_menu.keys: if "Transfer" in key.action: self._format_edge(n, key.phone_number, key.number) + def _format_edge(self, node_a: str, node_b: str, label: str): try: - self.dot.edge(node_a.id, node_b.id, label) + self.dot.edge(node_a.id, node_b.id, label, GraphvizModule.EDGE_STYLYING) except AttributeError: try: - self.dot.edge(node_a.id, node_b.service_user_id, label) + self.dot.edge(node_a.id, node_b.service_user_id, label, GraphvizModule.EDGE_STYLYING) except AttributeError: try: - self.dot.edge(node_a.service_user_id, node_b.service_user_id, label) + self.dot.edge(node_a.service_user_id, node_b.service_user_id, label, GraphvizModule.EDGE_STYLYING) except AttributeError: - self.dot.edge(node_a.service_user_id, node_b.id, label) + self.dot.edge(node_a.service_user_id, node_b.id, label, GraphvizModule.EDGE_STYLYING) def _save_graph(self, filename: str): diff --git a/odins_spear/reports/report_utils/parsing.py b/odins_spear/reports/report_utils/parsing.py index bdac31c..7babf62 100644 --- a/odins_spear/reports/report_utils/parsing.py +++ b/odins_spear/reports/report_utils/parsing.py @@ -1,4 +1,5 @@ from odins_spear.store import broadwork_entities as bre +from .report_entities import external_number # nodes needed for graph generation NODES = [] @@ -26,66 +27,104 @@ def _traverse_connecting_entities(entity: object, data_store: object, visited= N try: if isinstance(entity, bre.User): - entity.call_forwarding_always = data_store.number_mapping.get(entity.call_forwarding_always) - if entity.call_forwarding_always and entity.call_forwarding_always not in visited: - _traverse_connecting_entities(entity.call_forwarding_always, data_store, visited) + # Checks to see if the number is external first and creates external number object for parsing later + if data_store.number_mapping.get(entity.call_forwarding_always) == None and len(str(entity.call_forwarding_always)) >= 3 and str(entity.call_forwarding_always)[2].isdigit(): + entity.call_forwarding_always = external_number(str(entity.call_forwarding_always)) + else: + entity.call_forwarding_always = data_store.number_mapping.get(entity.call_forwarding_always) + if entity.call_forwarding_always and entity.call_forwarding_always not in visited: + _traverse_connecting_entities(entity.call_forwarding_always, data_store, visited) - entity.call_forwarding_busy = data_store.number_mapping.get(entity.call_forwarding_busy) - if entity.call_forwarding_busy and entity.call_forwarding_busy not in visited: - _traverse_connecting_entities(entity.call_forwarding_busy, data_store, visited) + if data_store.number_mapping.get(entity.call_forwarding_busy) == None and len(str(entity.call_forwarding_busy)) >= 3 and str(entity.call_forwarding_busy)[2].isdigit(): + entity.call_forwarding_busy = external_number(str(entity.call_forwarding_busy)) + else: + entity.call_forwarding_busy = data_store.number_mapping.get(entity.call_forwarding_busy) + if entity.call_forwarding_busy and entity.call_forwarding_busy not in visited: + _traverse_connecting_entities(entity.call_forwarding_busy, data_store, visited) - entity.call_forwarding_no_answer = data_store.number_mapping.get(entity.call_forwarding_no_answer) - if entity.call_forwarding_no_answer and entity.call_forwarding_no_answer not in visited: - _traverse_connecting_entities(entity.call_forwarding_no_answer, data_store, visited) + if data_store.number_mapping.get(entity.call_forwarding_no_answer) == None and len(str(entity.call_forwarding_no_answer)) >= 3 and str(entity.call_forwarding_no_answer)[2].isdigit(): + entity.call_forwarding_no_answer = external_number(str(entity.call_forwarding_no_answer)) + else: + entity.call_forwarding_no_answer = data_store.number_mapping.get(entity.call_forwarding_no_answer) + if entity.call_forwarding_no_answer and entity.call_forwarding_no_answer not in visited: + _traverse_connecting_entities(entity.call_forwarding_no_answer, data_store, visited) - entity.call_forwarding_not_reachable = data_store.number_mapping.get(entity.call_forwarding_not_reachable) - if entity.call_forwarding_not_reachable and entity.call_forwarding_not_reachable not in visited: - _traverse_connecting_entities(entity.call_forwarding_not_reachable, data_store, visited) + if data_store.number_mapping.get(entity.call_forwarding_not_reachable) == None and len(str(entity.call_forwarding_not_reachable)) >= 3 and str(entity.call_forwarding_not_reachable)[2].isdigit(): + entity.call_forwarding_not_reachable = external_number(str(entity.call_forwarding_not_reachable)) + else: + entity.call_forwarding_not_reachable = data_store.number_mapping.get(entity.call_forwarding_not_reachable) + if entity.call_forwarding_not_reachable and entity.call_forwarding_not_reachable not in visited: + _traverse_connecting_entities(entity.call_forwarding_not_reachable, data_store, visited) if isinstance(entity, bre.CallCenter): if entity.bounced_calls_enabled: - entity.bounced_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.bounced_calls_transfer_to_phone_number)) - if entity.bounced_calls_transfer_to_phone_number and entity.bounced_calls_transfer_to_phone_number not in visited: - _traverse_connecting_entities(entity.bounced_calls_transfer_to_phone_number, data_store, visited) + # Checks to see if the number is external first and creates external number object for parsing later + if data_store.number_mapping.get(str(entity.bounced_calls_transfer_to_phone_number)) == None and len(str(entity.bounced_calls_transfer_to_phone_number)) >= 3 and str(entity.bounced_calls_transfer_to_phone_number)[2].isdigit(): + entity.bounced_calls_transfer_to_phone_number = external_number(str(entity.bounced_calls_transfer_to_phone_number)) + else: + entity.bounced_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.bounced_calls_transfer_to_phone_number)) + if entity.bounced_calls_transfer_to_phone_number and entity.bounced_calls_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.bounced_calls_transfer_to_phone_number, data_store, visited) if entity.overflow_calls_action: - entity.overflow_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.overflow_calls_transfer_to_phone_number)) - if entity.overflow_calls_transfer_to_phone_number and entity.overflow_calls_transfer_to_phone_number not in visited: - _traverse_connecting_entities(entity.overflow_calls_transfer_to_phone_number, data_store, visited) + if data_store.number_mapping.get(str(entity.overflow_calls_transfer_to_phone_number)) == None and len(str(entity.call_forwarding_overflow_calls_transfer_to_phone_numberalways)) >= 3 and str(entity.overflow_calls_transfer_to_phone_number)[2].isdigit(): + entity.overflow_calls_transfer_to_phone_number = external_number(str(entity.overflow_calls_transfer_to_phone_number)) + else: + entity.overflow_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.overflow_calls_transfer_to_phone_number)) + if entity.overflow_calls_transfer_to_phone_number and entity.overflow_calls_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.overflow_calls_transfer_to_phone_number, data_store, visited) if entity.stranded_calls_action: - entity.stranded_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.stranded_calls_transfer_to_phone_number)) - if entity.stranded_calls_transfer_to_phone_number and entity.stranded_calls_transfer_to_phone_number not in visited: - _traverse_connecting_entities(entity.stranded_calls_transfer_to_phone_number, data_store, visited) + if data_store.number_mapping.get(str(entity.stranded_calls_transfer_to_phone_number)) == None and len(str(entity.stranded_calls_transfer_to_phone_number)) >= 3 and str(entity.stranded_calls_transfer_to_phone_number)[2].isdigit(): + entity.stranded_calls_transfer_to_phone_number = external_number(str(entity.stranded_calls_transfer_to_phone_number)) + else: + entity.stranded_calls_transfer_to_phone_number = data_store.number_mapping.get(str(entity.stranded_calls_transfer_to_phone_number)) + if entity.stranded_calls_transfer_to_phone_number and entity.stranded_calls_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.stranded_calls_transfer_to_phone_number, data_store, visited) if entity.stranded_call_unavailable_action: - entity.stranded_call_unavailable_transfer_to_phone_number = data_store.number_mapping.get(str(entity.stranded_call_unavailable_transfer_to_phone_number)) - if entity.stranded_call_unavailable_transfer_to_phone_number and entity.stranded_call_unavailable_transfer_to_phone_number not in visited: - _traverse_connecting_entities(entity.stranded_call_unavailable_transfer_to_phone_number, data_store, visited) + if data_store.number_mapping.get(str(entity.stranded_call_unavailable_transfer_to_phone_number)) == None and len(str(entity.stranded_call_unavailable_transfer_to_phone_number)) >= 3 and str(entity.stranded_call_unavailable_transfer_to_phone_number)[2].isdigit(): + entity.stranded_call_unavailable_transfer_to_phone_number = external_number(str(entity.stranded_call_unavailable_transfer_to_phone_number)) + else: + entity.stranded_call_unavailable_transfer_to_phone_number = data_store.number_mapping.get(str(entity.stranded_call_unavailable_transfer_to_phone_number)) + if entity.stranded_call_unavailable_transfer_to_phone_number and entity.stranded_call_unavailable_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.stranded_call_unavailable_transfer_to_phone_number, data_store, visited) if isinstance(entity, bre.HuntGroup): if entity.forward_after_timeout_enabled: - entity.no_answer_forward_to_phone_number = data_store.number_mapping.get(str(entity.no_answer_forward_to_phone_number)) - if entity.no_answer_forward_to_phone_number and entity.no_answer_forward_to_phone_number not in visited: - _traverse_connecting_entities(entity.no_answer_forward_to_phone_number, data_store, visited) + if data_store.number_mapping.get(entity.no_answer_forward_to_phone_number) is None and len(str(entity.no_answer_forward_to_phone_number)) >= 4 and str(entity.no_answer_forward_to_phone_number)[2].isdigit(): + entity.no_answer_forward_to_phone_number = external_number(str(entity.no_answer_forward_to_phone_number)) + else: + entity.no_answer_forward_to_phone_number = data_store.number_mapping.get(str(entity.no_answer_forward_to_phone_number)) + if entity.no_answer_forward_to_phone_number and entity.no_answer_forward_to_phone_number not in visited: + _traverse_connecting_entities(entity.no_answer_forward_to_phone_number, data_store, visited) if entity.call_forward_not_reachable_enabled: - entity.call_forward_not_reachable_transfer_to_phone_number = data_store.number_mapping.get(str(entity.call_forward_not_reachable_transfer_to_phone_number)) - if entity.call_forward_not_reachable_transfer_to_phone_number and entity.call_forward_not_reachable_transfer_to_phone_number not in visited: - _traverse_connecting_entities(entity.call_forward_not_reachable_transfer_to_phone_number, data_store, visited) + if data_store.number_mapping.get(str(entity.call_forward_not_reachable_transfer_to_phone_number)) == None and len(str(entity.call_forward_not_reachable_transfer_to_phone_number)) >= 3 and str(entity.call_forward_not_reachable_transfer_to_phone_number)[2].isdigit(): + entity.call_forward_not_reachable_transfer_to_phone_number = external_number(str(entity.call_forward_not_reachable_transfer_to_phone_number)) + else: + entity.call_forward_not_reachable_transfer_to_phone_number = data_store.number_mapping.get(str(entity.call_forward_not_reachable_transfer_to_phone_number)) + if entity.call_forward_not_reachable_transfer_to_phone_number and entity.call_forward_not_reachable_transfer_to_phone_number not in visited: + _traverse_connecting_entities(entity.call_forward_not_reachable_transfer_to_phone_number, data_store, visited) if isinstance(entity, bre.AutoAttendant): for key in entity.business_hours_menu.keys: if "Transfer" in key.action: - key.phone_number = data_store.number_mapping.get(str(key.phone_number)) - if key.phone_number and key.phone_number not in visited: - _traverse_connecting_entities(key.phone_number, data_store, visited) + if data_store.number_mapping.get(str(key.phone_number)) == None and len(str(key.phone_number)) >= 3 and str(key.phone_number)[2].isdigit(): + key.phone_number = external_number(str(key.phone_number)) + else: + key.phone_number = data_store.number_mapping.get(str(key.phone_number)) + if key.phone_number and key.phone_number not in visited: + _traverse_connecting_entities(key.phone_number, data_store, visited) for key in entity.after_hours_menu.keys: if "Transfer" in key.action: - key.phone_number = data_store.number_mapping.get(str(key.phone_number)) - if key.phone_number and key.phone_number not in visited: - _traverse_connecting_entities(key.phone_number, data_store, visited) + if data_store.number_mapping.get(str(key.phone_number)) == None and len(str(key.phone_number)) >= 3 and str(key.phone_number)[2].isdigit(): + key.phone_number = external_number(str(key.phone_number)) + else: + key.phone_number = data_store.number_mapping.get(str(key.phone_number)) + if key.phone_number and key.phone_number not in visited: + _traverse_connecting_entities(key.phone_number, data_store, visited) except TypeError: # this object is further up the tree and a loop has occured - Move one and come back pass diff --git a/odins_spear/reports/report_utils/report_entities.py b/odins_spear/reports/report_utils/report_entities.py index d4c856b..94375cb 100644 --- a/odins_spear/reports/report_utils/report_entities.py +++ b/odins_spear/reports/report_utils/report_entities.py @@ -1,11 +1,12 @@ +from dataclasses import dataclass, field +from typing import List, Type + +@dataclass class call_flow: - def __init__( - self, name, nodes: list) -> None: - """Usually a call flow to a number and how calls to this number flows through the system. + name: str + nodes: List = field(default_factory=list) - :param name: Flow name. - :param nodes: List of the nodes that make up the flow. - """ - self.name = name - self.nodes = nodes \ No newline at end of file +@dataclass +class external_number: + id: str \ No newline at end of file diff --git a/os_reports/Calls To 3213061544.svg b/os_reports/Calls To 3213061544.svg index fc5cec2..7096366 100644 --- a/os_reports/Calls To 3213061544.svg +++ b/os_reports/Calls To 3213061544.svg @@ -1,150 +1,117 @@ - - - - -3213061544 + + + +3213061544 Start - -Start - - - -JRDNEVA101@jrdneva.ev.com - -101 - - - -Start->JRDNEVA101@jrdneva.ev.com - - + +Start - + basic_aa@jrdneva.ev.com - -3001 + +3001 - - -basic_aa@jrdneva.ev.com->JRDNEVA101@jrdneva.ev.com - - -0 + + +Start->basic_aa@jrdneva.ev.com + + + + + +JRDNEVA104@jrdneva.ev.com + +104 - + testing@jrdneva.ev.com - -2001 + +2001 + + + +JRDNEVA104@jrdneva.ev.com->testing@jrdneva.ev.com + + +CFB - + JRDNEVA102@jrdneva.ev.com - -102 + +102 + + + +testing@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com + + +NACF testing@jrdneva.ev.com->JRDNEVA102@jrdneva.ev.com - - -CFNR + + +CFNR - - -testing@jrdneva.ev.com->JRDNEVA101@jrdneva.ev.com - - -NACF - - - -JRDNEVA104@jrdneva.ev.com - -104 + + +basic_cc@jrdneva.ev.com + +1001 - + -JRDNEVA104@jrdneva.ev.com->basic_aa@jrdneva.ev.com - - -CFA - - - -JRDNEVA104@jrdneva.ev.com->testing@jrdneva.ev.com - - -CFB +basic_cc@jrdneva.ev.com->testing@jrdneva.ev.com + + +OF - - -JRDNEVA141401_EVA_EL@jrdneva.ev.com - -141401 + + +basic_aa@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com + + +2 - - -basic_cc@jrdneva.ev.com - -1001 + + +basic_aa@jrdneva.ev.com->testing@jrdneva.ev.com + + +1 - + -basic_cc@jrdneva.ev.com->JRDNEVA141401_EVA_EL@jrdneva.ev.com - - -OF +basic_aa@jrdneva.ev.com->basic_cc@jrdneva.ev.com + + +0 - - -JRDNEVA102@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com - - -CFA + + +0123456789 + +0123456789 - - -JRDNEVA102@jrdneva.ev.com->basic_cc@jrdneva.ev.com - - -CFB - - - -JRDNEVA103@jrdneva.ev.com - -103 - - + -JRDNEVA103@jrdneva.ev.com->basic_cc@jrdneva.ev.com - - -CFB - - - -JRDNEVA101@jrdneva.ev.com->JRDNEVA102@jrdneva.ev.com - - -CFB - - - -JRDNEVA101@jrdneva.ev.com->JRDNEVA103@jrdneva.ev.com - - -CFNR +basic_aa@jrdneva.ev.com->0123456789 + + +3 From 76cdb9ba73e52bfa2df1dc054060736c6f1237d6 Mon Sep 17 00:00:00 2001 From: Jordan-Prescott Date: Thu, 16 May 2024 17:03:51 +0100 Subject: [PATCH 24/25] External number will get default node --- .../reports/report_utils/graphviz_module.py | 18 ++-- os_reports/Calls To 3213061544.svg | 90 +++++++++---------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/odins_spear/reports/report_utils/graphviz_module.py b/odins_spear/reports/report_utils/graphviz_module.py index ab1f8d1..a9c9233 100644 --- a/odins_spear/reports/report_utils/graphviz_module.py +++ b/odins_spear/reports/report_utils/graphviz_module.py @@ -24,7 +24,7 @@ class GraphvizModule: 'fontcolor': 'white' }, 'exit': { - 'shape': 'box', + 'shape': 'record', 'style': 'filled', 'margin': '0.2', 'color': '#020300', @@ -32,13 +32,13 @@ class GraphvizModule: 'fontcolor': 'white' }, 'auto_attendant': { - 'shape': 'circle', - 'style': 'filled', - 'margin': '0.2', - 'color': '#5E1008', - 'fillcolor': '#FF0000', - 'fontname': 'Arial', - 'fontcolor': 'white' + 'shape': 'circle', + 'style': 'filled', + 'margin': '0.2', + 'color': '#5E1008', + 'fillcolor': '#FF0000', + 'fontname': 'Arial', + 'fontcolor': 'white' }, 'call_centre': { 'shape': 'Mrecord', @@ -94,8 +94,6 @@ def generate_call_flow_graph(self, nodes: list, number: str): self.dot.node(n.service_user_id, n.extension, GraphvizModule.NODE_STYLING["hunt_group"]) elif isinstance(n, bre.AutoAttendant): self.dot.node(n.service_user_id, n.extension, GraphvizModule.NODE_STYLING["auto_attendant"]) - elif isinstance(n, external_number): - self.dot.node(n.id, n.id, GraphvizModule.NODE_STYLING["exit"]) # build edges for n in nodes: diff --git a/os_reports/Calls To 3213061544.svg b/os_reports/Calls To 3213061544.svg index 7096366..55b187f 100644 --- a/os_reports/Calls To 3213061544.svg +++ b/os_reports/Calls To 3213061544.svg @@ -4,114 +4,114 @@ - - - -3213061544 + + + +3213061544 Start - -Start + +Start basic_aa@jrdneva.ev.com - -3001 + +3001 Start->basic_aa@jrdneva.ev.com - - + + JRDNEVA104@jrdneva.ev.com - -104 + +104 testing@jrdneva.ev.com - -2001 + +2001 JRDNEVA104@jrdneva.ev.com->testing@jrdneva.ev.com - - -CFB + + +CFB JRDNEVA102@jrdneva.ev.com - -102 + +102 testing@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com - - -NACF + + +NACF testing@jrdneva.ev.com->JRDNEVA102@jrdneva.ev.com - - -CFNR + + +CFNR basic_cc@jrdneva.ev.com - -1001 + +1001 basic_cc@jrdneva.ev.com->testing@jrdneva.ev.com - - -OF + + +OF basic_aa@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com - - -2 + + +2 basic_aa@jrdneva.ev.com->testing@jrdneva.ev.com - - -1 + + +1 basic_aa@jrdneva.ev.com->basic_cc@jrdneva.ev.com - - -0 + + +0 0123456789 - -0123456789 + +0123456789 basic_aa@jrdneva.ev.com->0123456789 - - -3 + + +3 From 1fcfb6cf489b01c435538ef2c4c32870794b7b6d Mon Sep 17 00:00:00 2001 From: Jordan Prescott Date: Fri, 17 May 2024 10:27:48 +0100 Subject: [PATCH 25/25] Delete Calls To 3213061544.svg --- os_reports/Calls To 3213061544.svg | 117 ----------------------------- 1 file changed, 117 deletions(-) delete mode 100644 os_reports/Calls To 3213061544.svg diff --git a/os_reports/Calls To 3213061544.svg b/os_reports/Calls To 3213061544.svg deleted file mode 100644 index 55b187f..0000000 --- a/os_reports/Calls To 3213061544.svg +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - -3213061544 - - -Start - -Start - - - -basic_aa@jrdneva.ev.com - -3001 - - - -Start->basic_aa@jrdneva.ev.com - - - - - -JRDNEVA104@jrdneva.ev.com - -104 - - - -testing@jrdneva.ev.com - -2001 - - - -JRDNEVA104@jrdneva.ev.com->testing@jrdneva.ev.com - - -CFB - - - -JRDNEVA102@jrdneva.ev.com - -102 - - - -testing@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com - - -NACF - - - -testing@jrdneva.ev.com->JRDNEVA102@jrdneva.ev.com - - -CFNR - - - -basic_cc@jrdneva.ev.com - -1001 - - - -basic_cc@jrdneva.ev.com->testing@jrdneva.ev.com - - -OF - - - -basic_aa@jrdneva.ev.com->JRDNEVA104@jrdneva.ev.com - - -2 - - - -basic_aa@jrdneva.ev.com->testing@jrdneva.ev.com - - -1 - - - -basic_aa@jrdneva.ev.com->basic_cc@jrdneva.ev.com - - -0 - - - -0123456789 - -0123456789 - - - -basic_aa@jrdneva.ev.com->0123456789 - - -3 - - -