medicafe 0.240415.1__py3-none-any.whl → 0.240517.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of medicafe might be problematic. Click here for more details.

Files changed (42) hide show
  1. MediBot/MediBot.bat +198 -0
  2. MediBot/MediBot.py +346 -0
  3. MediBot/MediBot_Charges.py +28 -0
  4. MediBot/MediBot_Crosswalk_Library.py +280 -0
  5. MediBot/MediBot_Preprocessor.py +247 -0
  6. MediBot/MediBot_Preprocessor_lib.py +357 -0
  7. MediBot/MediBot_UI.py +240 -0
  8. MediBot/MediBot_dataformat_library.py +198 -0
  9. MediBot/MediBot_docx_decoder.py +80 -0
  10. MediBot/MediPost.py +5 -0
  11. MediBot/PDF_to_CSV_Cleaner.py +211 -0
  12. MediBot/__init__.py +0 -0
  13. MediBot/update_json.py +43 -0
  14. MediBot/update_medicafe.py +57 -0
  15. MediLink/MediLink.py +381 -0
  16. MediLink/MediLink_277_decoder.py +92 -0
  17. MediLink/MediLink_837p_encoder.py +502 -0
  18. MediLink/MediLink_837p_encoder_library.py +890 -0
  19. MediLink/MediLink_API_v2.py +174 -0
  20. MediLink/MediLink_APIs.py +137 -0
  21. MediLink/MediLink_ConfigLoader.py +81 -0
  22. MediLink/MediLink_DataMgmt.py +258 -0
  23. MediLink/MediLink_Down.py +128 -0
  24. MediLink/MediLink_ERA_decoder.py +192 -0
  25. MediLink/MediLink_Gmail.py +100 -0
  26. MediLink/MediLink_Mailer.py +7 -0
  27. MediLink/MediLink_Scheduler.py +173 -0
  28. MediLink/MediLink_StatusCheck.py +4 -0
  29. MediLink/MediLink_UI.py +118 -0
  30. MediLink/MediLink_Up.py +383 -0
  31. MediLink/MediLink_batch.bat +7 -0
  32. MediLink/Soumit_api.py +22 -0
  33. MediLink/__init__.py +0 -0
  34. MediLink/test.py +74 -0
  35. medicafe-0.240517.0.dist-info/METADATA +53 -0
  36. medicafe-0.240517.0.dist-info/RECORD +39 -0
  37. {medicafe-0.240415.1.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +1 -1
  38. medicafe-0.240517.0.dist-info/top_level.txt +2 -0
  39. medicafe-0.240415.1.dist-info/METADATA +0 -17
  40. medicafe-0.240415.1.dist-info/RECORD +0 -5
  41. medicafe-0.240415.1.dist-info/top_level.txt +0 -1
  42. {medicafe-0.240415.1.dist-info → medicafe-0.240517.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,174 @@
1
+ import time
2
+ import requests
3
+
4
+ # Importing configuration loader
5
+ try:
6
+ from MediLink import MediLink_ConfigLoader
7
+ except ImportError:
8
+ import MediLink_ConfigLoader
9
+
10
+ # Class for handling API calls
11
+ class APIClient:
12
+ def __init__(self):
13
+ # Load configuration
14
+ self.config, _ = MediLink_ConfigLoader.load_configuration()
15
+ # Initialize token cache
16
+ self.token_cache = {}
17
+
18
+ # Method to get access token
19
+ def get_access_token(self, endpoint_name):
20
+ # Retrieve endpoint configuration
21
+ endpoint_config = self.config['MediLink_Config']['endpoints'][endpoint_name]
22
+ current_time = time.time()
23
+
24
+ # Check if token is cached and still valid
25
+ if endpoint_name in self.token_cache:
26
+ cached_token = self.token_cache[endpoint_name]
27
+ if cached_token['expires_at'] > current_time:
28
+ return cached_token['access_token']
29
+
30
+ # Prepare data for token request
31
+ data = {
32
+ 'grant_type': 'client_credentials',
33
+ 'client_id': endpoint_config['client_id'],
34
+ 'client_secret': endpoint_config['client_secret'],
35
+ 'scope': 'hipaa'
36
+ }
37
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
38
+
39
+ # Request token
40
+ response = requests.post(endpoint_config['token_url'], headers=headers, data=data)
41
+ response.raise_for_status()
42
+ token_data = response.json()
43
+ access_token = token_data['access_token']
44
+ expires_in = token_data.get('expires_in', 3600)
45
+
46
+ # Cache token with expiration time adjusted
47
+ self.token_cache[endpoint_name] = {
48
+ 'access_token': access_token,
49
+ 'expires_at': current_time + expires_in - 120
50
+ }
51
+ return access_token
52
+
53
+ # Method for making API calls
54
+ def make_api_call(self, endpoint_name, call_type, url_extension="", params=None, data=None):
55
+ # Retrieve endpoint configuration
56
+ endpoint_config = self.config['MediLink_Config']['endpoints'][endpoint_name]
57
+ # Get access token
58
+ token = self.get_access_token(endpoint_name)
59
+ headers = {'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'}
60
+
61
+ # Construct full URL
62
+ url = endpoint_config['api_url'] + url_extension
63
+
64
+ # Make appropriate type of call
65
+ if call_type == 'GET':
66
+ response = requests.get(url, headers=headers, params=params)
67
+ elif call_type == 'POST':
68
+ headers['Content-Type'] = 'application/json'
69
+ response = requests.post(url, headers=headers, json=data)
70
+ elif call_type == 'DELETE':
71
+ response = requests.delete(url, headers=headers)
72
+ else:
73
+ raise ValueError("Unsupported call type")
74
+
75
+ if response.status_code >= 400:
76
+ print("Error {}: {}".format(response.status_code, response.text))
77
+ response.raise_for_status()
78
+
79
+ return response.json()
80
+
81
+ # Method for creating coverage
82
+ def create_coverage(self, endpoint_name, patient_info):
83
+ return self.make_api_call(endpoint_name, 'POST', url_extension="/coverages", data=patient_info)
84
+
85
+ # Method for getting all coverages
86
+ def get_coverages(self, endpoint_name, params=None):
87
+ return self.make_api_call(endpoint_name, 'GET', url_extension="/coverages", params=params)
88
+
89
+ # Method for getting coverage by ID
90
+ def get_coverage_by_id(self, endpoint_name, coverage_id):
91
+ return self.make_api_call(endpoint_name, 'GET', url_extension="/coverages/{}".format(coverage_id))
92
+
93
+ # Method for deleting coverage by ID
94
+ def delete_coverage_by_id(self, endpoint_name, coverage_id):
95
+ return self.make_api_call(endpoint_name, 'DELETE', url_extension="/coverages/{}".format(coverage_id))
96
+
97
+ # Method for getting payer list
98
+ def get_payer_list(self, endpoint_name, params=None):
99
+ return self.make_api_call(endpoint_name, 'GET', url_extension="/availity-payer-list", params=params)
100
+
101
+ # Function to fetch payer name from API
102
+ def fetch_payer_name_from_api(payer_id, config, primary_endpoint='AVAILITY'):
103
+ client = APIClient()
104
+
105
+ # Step 1: Reload configuration for safety (This should be able to be replaced by APIClient())
106
+ config, _ = MediLink_ConfigLoader.load_configuration()
107
+
108
+ # Step 2: Check if the primary endpoint is specified and is valid
109
+ # (these validity checks will need to be incorporated into the main functionality)
110
+ endpoints = config['MediLink_Config']['endpoints']
111
+ if primary_endpoint and primary_endpoint in endpoints:
112
+ endpoint_order = [primary_endpoint] + [endpoint for endpoint in endpoints if endpoint != primary_endpoint]
113
+ else:
114
+ endpoint_order = list(endpoints.keys())
115
+
116
+ # Step 3: Iterate through available endpoints in specified order
117
+ for endpoint_name in endpoint_order:
118
+ endpoint_config = endpoints[endpoint_name]
119
+ if not all(key in endpoint_config for key in ['token_url', 'client_id', 'client_secret']):
120
+ MediLink_ConfigLoader.log("Skipping {} due to missing API keys.".format(endpoint_name), config, level="WARNING")
121
+ continue
122
+
123
+ try:
124
+ response = client.get_payer_list(endpoint_name, params={'payerId': payer_id})
125
+ if 'payers' in response and response['payers']:
126
+ payer = response['payers'][0]
127
+ payer_name = payer.get('displayName') or payer.get('name')
128
+
129
+ MediLink_ConfigLoader.log("Successfully found payer at {} for ID {}: {}".format(endpoint_name, payer_id, payer_name), config, level="INFO")
130
+ return payer_name
131
+ else:
132
+ MediLink_ConfigLoader.log("No payer found at {} for ID: {}. Trying next available endpoint.".format(endpoint_name, payer_id), config, level="INFO")
133
+ except requests.RequestException as e:
134
+ MediLink_ConfigLoader.log("API call failed at {} for Payer ID '{}': {}".format(endpoint_name, payer_id, str(e)), config, level="ERROR")
135
+
136
+ error_message = "All endpoints exhausted for Payer ID {}.".format(payer_id)
137
+ MediLink_ConfigLoader.log(error_message, config, level="CRITICAL")
138
+ raise ValueError(error_message)
139
+
140
+ # Example usage
141
+ if __name__ == "__main__":
142
+ client = APIClient()
143
+ try:
144
+ # Fetch and print payer name
145
+ payer_name = fetch_payer_name_from_api("11347", 'config.yaml')
146
+ print("Payer Name: {}".format(payer_name))
147
+
148
+ # Example patient info
149
+ patient_info = {
150
+ "policyNumber": "12345",
151
+ "name": "John Doe",
152
+ "dob": "1980-01-01"
153
+ }
154
+ # Create coverage and print response
155
+ response = client.create_coverage('AVAILITY', patient_info)
156
+ print("Create Coverage Response: {}".format(response))
157
+
158
+ # Get all coverages and print response
159
+ response = client.get_coverages('AVAILITY')
160
+ print("All Coverages: {}".format(response))
161
+
162
+ # Example coverage ID
163
+ coverage_id = "some-coverage-id"
164
+ # Get coverage by ID and print response
165
+ response = client.get_coverage_by_id('AVAILITY', coverage_id)
166
+ print("Coverage by ID: {}".format(response))
167
+
168
+ # Delete coverage by ID and print response
169
+ response = client.delete_coverage_by_id('AVAILITY', coverage_id)
170
+ print("Delete Coverage Response: {}".format(response))
171
+
172
+ except Exception as e:
173
+ # Print error if any
174
+ print("Error: {}".format(e))
@@ -0,0 +1,137 @@
1
+ import time
2
+ import requests
3
+ try:
4
+ from MediLink import MediLink_ConfigLoader
5
+ except ImportError:
6
+ import MediLink_ConfigLoader
7
+
8
+ # Fetches the payer name from API based on the payer ID.
9
+ def fetch_payer_name_from_api(payer_id, config, primary_endpoint='AVAILITY'):
10
+ """
11
+ Fetches the payer name from the API using the provided payer ID.
12
+
13
+ Args:
14
+ payer_id (str): The ID of the payer.
15
+ config (dict): Configuration settings.
16
+ primary_endpoint (str): The primary endpoint for resolving payer information.
17
+
18
+ Raises:
19
+ ValueError: If all endpoints are exhausted without finding the payer.
20
+
21
+ Returns:
22
+ str: The fetched payer name.
23
+ """
24
+ # Reload for safety
25
+ config, _ = MediLink_ConfigLoader.load_configuration()
26
+
27
+ # Step 1: Retrieve endpoint configurations
28
+ endpoints = config['MediLink_Config']['endpoints']
29
+ tried_endpoints = []
30
+
31
+ # Step 2: Check if the primary endpoint is specified and is valid
32
+ if primary_endpoint and primary_endpoint in endpoints:
33
+ endpoint_order = [primary_endpoint] + [endpoint for endpoint in endpoints if endpoint != primary_endpoint]
34
+ else:
35
+ endpoint_order = list(endpoints.keys())
36
+
37
+ # Step 3: Iterate through available endpoints in specified order
38
+ for endpoint_name in endpoint_order:
39
+ endpoint_config = endpoints[endpoint_name]
40
+ if not all(key in endpoint_config for key in ['token_url', 'client_id', 'client_secret']):
41
+ MediLink_ConfigLoader.log("Skipping {} due to missing API keys.".format(endpoint_name), config, level="WARNING")
42
+ continue
43
+
44
+ # Step 4: Get access token for the endpoint
45
+ token = get_access_token(endpoint_config)
46
+ api_url = endpoint_config.get("api_url", "")
47
+ headers = {'Authorization': 'Bearer {}'.format(token), 'Accept': 'application/json'}
48
+ params = {'payerId': payer_id}
49
+
50
+ try:
51
+ # Step 5: Make API call to fetch payer name
52
+ response = requests.get(api_url, headers=headers, params=params)
53
+ response.raise_for_status()
54
+ data = response.json()
55
+ if 'payers' in data and data['payers']:
56
+ payer = data['payers'][0]
57
+ payer_name = payer.get('displayName') or payer.get('name')
58
+
59
+ # Log successful match
60
+ MediLink_ConfigLoader.log("Successfully found payer at {} for ID {}: {}".format(endpoint_name, payer_id, payer_name), config, level="INFO")
61
+
62
+ return payer_name
63
+ else:
64
+ MediLink_ConfigLoader.log("No payer found at {} for ID: {}. Trying next available endpoint.".format(endpoint_name, payer_id), config, level="INFO")
65
+ except requests.RequestException as e:
66
+ # Step 6: Log API call failure
67
+ MediLink_ConfigLoader.log("API call failed at {} for Payer ID '{}': {}".format(endpoint_name, payer_id, str(e)), config, level="ERROR")
68
+ tried_endpoints.append(endpoint_name)
69
+
70
+ # Step 7: Log all endpoints exhaustion and raise error
71
+ error_message = "All endpoints exhausted for Payer ID {}. Endpoints tried: {}".format(payer_id, ', '.join(tried_endpoints))
72
+ MediLink_ConfigLoader.log(error_message, config, level="CRITICAL")
73
+ raise ValueError(error_message)
74
+
75
+ # Test Case for API fetch
76
+ #payer_id = "11347"
77
+ #config = load_configuration()
78
+ #payer_name = fetch_payer_name_from_api(payer_id, config, endpoint='AVAILITY')
79
+ #print(payer_id, payer_name)
80
+
81
+ # Initialize a global dictionary to store the access token and its expiry time
82
+ # TODO (Low API) This will need to get setup for each endpoint separately.
83
+ token_cache = {
84
+ 'access_token': None,
85
+ 'expires_at': 0 # Timestamp of when the token expires
86
+ }
87
+
88
+ def get_access_token(endpoint_config):
89
+ current_time = time.time()
90
+
91
+ # Check if the cached token is still valid
92
+ if token_cache['access_token'] and token_cache['expires_at'] > current_time:
93
+ return token_cache['access_token']
94
+
95
+ # Validate endpoint configuration
96
+ if not endpoint_config or not all(k in endpoint_config for k in ['client_id', 'client_secret', 'token_url']):
97
+ raise ValueError("Endpoint configuration is incomplete or missing necessary fields.")
98
+
99
+ # Extract credentials and URL from the config
100
+ CLIENT_ID = endpoint_config.get("client_id")
101
+ CLIENT_SECRET = endpoint_config.get("client_secret")
102
+ url = endpoint_config.get("token_url")
103
+
104
+ # Setup the data payload and headers for the HTTP request
105
+ data = {
106
+ 'grant_type': 'client_credentials',
107
+ 'client_id': CLIENT_ID,
108
+ 'client_secret': CLIENT_SECRET,
109
+ 'scope': 'hipaa'
110
+ }
111
+ headers = {
112
+ 'Content-Type': 'application/x-www-form-urlencoded'
113
+ }
114
+
115
+ try:
116
+ # Perform the HTTP request to get the access token
117
+ response = requests.post(url, headers=headers, data=data)
118
+ response.raise_for_status() # This will raise an exception for HTTP error statuses
119
+ json_response = response.json()
120
+ access_token = json_response.get('access_token')
121
+ expires_in = json_response.get('expires_in', 3600) # Default to 3600 seconds if not provided
122
+
123
+ if not access_token:
124
+ raise ValueError("No access token returned by the server.")
125
+
126
+ # Store the access token and calculate the expiry time
127
+ token_cache['access_token'] = access_token
128
+ token_cache['expires_at'] = current_time + expires_in - 120 # Subtracting 120 seconds to provide buffer
129
+
130
+ return access_token
131
+ except requests.RequestException as e:
132
+ # Handle HTTP errors (e.g., network problems, invalid response)
133
+ error_msg = "Failed to retrieve access token: {0}. Response status: {1}".format(str(e), response.status_code if response else 'No response')
134
+ raise Exception(error_msg)
135
+ except ValueError as e:
136
+ # Handle specific errors like missing access token
137
+ raise Exception("Configuration or server response error: {0}".format(str(e)))
@@ -0,0 +1,81 @@
1
+ import os
2
+ import json
3
+ import logging
4
+ from datetime import datetime
5
+ from collections import OrderedDict
6
+ import sys
7
+ import platform
8
+
9
+ """
10
+ This function should be generalizable to have a initialization script over all the Medi* functions
11
+ """
12
+ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'config.json'), crosswalk_path=os.path.join(os.path.dirname(__file__), '..', 'json', 'crosswalk.json')):
13
+ """
14
+ Loads endpoint configuration, credentials, and other settings from JSON files.
15
+
16
+ Returns: A tuple containing dictionaries with configuration settings for the main config and crosswalk.
17
+ """
18
+ # TODO (Low Config Upgrade) The Medicare / Private differentiator flag probably needs to be pulled or passed to this.
19
+ # BUG (HARDCODE) FOR NOW:
20
+ # Detect the operating system
21
+ if platform.system() == 'Windows' and platform.release() == 'XP':
22
+ # Use F: paths for Windows XP
23
+ config_path = "F:\\Medibot\\json\\config.json"
24
+ crosswalk_path = "F:\\Medibot\\json\\crosswalk.json"
25
+ else:
26
+ # Use G: paths for other versions of Windows
27
+ config_path = "G:\\My Drive\\Codes\\MediCafe\\json\\config.json"
28
+ crosswalk_path = "G:\\My Drive\\Codes\\MediCafe\\json\\crosswalk.json"
29
+
30
+ try:
31
+ with open(config_path, 'r') as config_file:
32
+ config = json.load(config_file, object_pairs_hook=OrderedDict)
33
+ if 'MediLink_Config' not in config:
34
+ raise KeyError("MediLink_Config key is missing from the loaded configuration.")
35
+ # MediLink_config = config['MediLink_Config']
36
+
37
+ with open(crosswalk_path, 'r') as crosswalk_file:
38
+ crosswalk = json.load(crosswalk_file)
39
+
40
+ return config, crosswalk
41
+ except ValueError as e:
42
+ if isinstance(e, UnicodeDecodeError):
43
+ print("Error decoding JSON file: {}".format(e))
44
+ else:
45
+ print("Error parsing JSON file: {}".format(e))
46
+ sys.exit(1) # Exit the script due to a critical error in configuration loading
47
+ except FileNotFoundError:
48
+ print("One or both JSON files not found. Config: {}, Crosswalk: {}".format(config_path, crosswalk_path))
49
+ sys.exit(1) # Exit the script due to a critical error in configuration loading
50
+ except KeyError as e:
51
+ print("Critical configuration is missing: {}".format(e))
52
+ sys.exit(1) # Exit the script due to a critical error in configuration loading
53
+ except Exception as e:
54
+ print("An unexpected error occurred while loading the configuration: {}".format(e))
55
+ sys.exit(1) # Exit the script due to a critical error in configuration loading
56
+
57
+ # Logs messages with optional error type and claim data.
58
+ def log(message, config=None, level="INFO", error_type=None, claim=None):
59
+
60
+ # If config is not provided, load it
61
+ if config is None:
62
+ config, _ = load_configuration()
63
+
64
+ # Setup logger if not already configured
65
+ if not logging.root.handlers:
66
+ local_storage_path = config['MediLink_Config'].get('local_storage_path', '.') if isinstance(config, dict) else '.'
67
+ log_filename = datetime.now().strftime("Log_%m%d%Y.log")
68
+ log_filepath = os.path.join(local_storage_path, log_filename)
69
+ logging.basicConfig(level=logging.INFO,
70
+ format='%(asctime)s - %(levelname)s - %(message)s',
71
+ filename=log_filepath,
72
+ filemode='a')
73
+
74
+ # Prepare log message
75
+ claim_data = " - Claim Data: {}".format(claim) if claim else ""
76
+ error_info = " - Error Type: {}".format(error_type) if error_type else ""
77
+ full_message = "{} {}{}".format(message, claim_data, error_info)
78
+
79
+ # Log the message
80
+ logger = logging.getLogger()
81
+ getattr(logger, level.lower())(full_message)
@@ -0,0 +1,258 @@
1
+ import csv
2
+ import os
3
+ from datetime import datetime, timedelta
4
+ import subprocess
5
+
6
+ # Need this for running Medibot and MediLink
7
+ try:
8
+ import MediLink_ConfigLoader
9
+ except ImportError:
10
+ from . import MediLink_ConfigLoader
11
+
12
+ # Helper function to slice and strip values
13
+ def slice_data(data, slices):
14
+ # Convert slices list to a tuple for slicing operation
15
+ return {key: data[slice(*slices[key])].strip() for key in slices}
16
+
17
+ # Function to parse fixed-width Medisoft output and extract claim data
18
+ def parse_fixed_width_data(personal_info, insurance_info, service_info, config=None):
19
+
20
+ # Make sure we have the right config
21
+ if not config: # Checks if config is None or an empty dictionary
22
+ MediLink_ConfigLoader.log("No config passed to parse_fixed_width_data. Re-loading config...", level="WARNING")
23
+ config, _ = MediLink_ConfigLoader.load_configuration()
24
+
25
+ config = config.get('MediLink_Config', config) # Safest config call.
26
+
27
+ # Load slice definitions from config within the MediLink_Config section
28
+ personal_slices = config['fixedWidthSlices']['personal_slices']
29
+ insurance_slices = config['fixedWidthSlices']['insurance_slices']
30
+ service_slices = config['fixedWidthSlices']['service_slices']
31
+
32
+ # Parse each segment
33
+ parsed_data = {}
34
+ parsed_data.update(slice_data(personal_info, personal_slices))
35
+ parsed_data.update(slice_data(insurance_info, insurance_slices))
36
+ parsed_data.update(slice_data(service_info, service_slices))
37
+
38
+ MediLink_ConfigLoader.log("Successfully parsed data from segments", config, level="INFO")
39
+
40
+ return parsed_data
41
+
42
+ # Function to read fixed-width Medisoft output and extract claim data
43
+ def read_fixed_width_data(file_path):
44
+ # Reads the fixed width data from the file and yields each patient's
45
+ # personal, insurance, and service information.
46
+ MediLink_ConfigLoader.log("Starting to read fixed width data...")
47
+ with open(file_path, 'r') as file:
48
+ lines_buffer = [] # Buffer to hold lines for current patient data
49
+ for line in file:
50
+ stripped_line = line.strip()
51
+ if stripped_line: # Only process non-empty lines
52
+ lines_buffer.append(stripped_line)
53
+ # Once we have 3 lines of data, yield them as a patient record
54
+ if len(lines_buffer) == 3:
55
+ personal_info, insurance_info, service_info = lines_buffer
56
+ MediLink_ConfigLoader.log("Successfully read data from file: {}".format(file_path), level="INFO")
57
+ yield personal_info, insurance_info, service_info
58
+ lines_buffer.clear() # Reset buffer for the next patient record
59
+ # If the line is blank but we have already started collecting a patient record,
60
+ # we continue without resetting the buffer, effectively skipping blank lines.
61
+
62
+ # TODO (Refactor) Consider consolidating with the other read_fixed_with_data
63
+ def read_general_fixed_width_data(file_path, slices):
64
+ # handle any fixed-width data based on provided slice definitions
65
+ with open(file_path, 'r', encoding='utf-8') as file:
66
+ next(file) # Skip the header
67
+ for line_number, line in enumerate(file, start=1):
68
+ insurance_name = {key: line[start:end].strip() for key, (start, end) in slices.items()}
69
+ yield insurance_name, line_number
70
+
71
+ def consolidate_csvs(source_directory):
72
+ """
73
+ This default overwrites any existing CSV for the same day. We want this for the automated runs but want to switch through
74
+ the user interaction option if we're running interactive. This has not been implemented, but the helper function exists.
75
+ """
76
+ today = datetime.now()
77
+ consolidated_filename = today.strftime("ERA_%m%d%y.csv")
78
+ consolidated_filepath = os.path.join(source_directory, consolidated_filename)
79
+
80
+ consolidated_data = []
81
+ header_saved = False
82
+
83
+ # Check if the file already exists and log the action
84
+ if os.path.exists(consolidated_filepath):
85
+ MediLink_ConfigLoader.log("The file {} already exists. It will be overwritten.".format(consolidated_filename))
86
+
87
+ for filename in os.listdir(source_directory):
88
+ filepath = os.path.join(source_directory, filename)
89
+ if not filepath.endswith('.csv') or os.path.isdir(filepath) or filepath == consolidated_filepath:
90
+ continue # Skip non-CSV files, directories, and the target consolidated file itself
91
+
92
+ # Check if the file was created within the last day
93
+ modification_time = datetime.fromtimestamp(os.path.getmtime(filepath))
94
+ if modification_time < today - timedelta(days=1):
95
+ continue # Skip files not modified in the last day
96
+
97
+ # Read and append data from each CSV
98
+ with open(filepath, 'r', newline='') as csvfile:
99
+ reader = csv.reader(csvfile)
100
+ header = next(reader) # Assumes all CSV files have the same header
101
+ if not header_saved: # Save header from the first file
102
+ consolidated_data.append(header)
103
+ header_saved = True
104
+ consolidated_data.extend(row for row in reader)
105
+
106
+ # Delete the source file after its contents have been added to the consolidation list
107
+ os.remove(filepath)
108
+
109
+ # Write consolidated data to a new or existing CSV file, overwriting it if it exists
110
+ with open(consolidated_filepath, 'w', newline='') as csvfile:
111
+ writer = csv.writer(csvfile)
112
+ writer.writerows(consolidated_data)
113
+
114
+ MediLink_ConfigLoader.log("Consolidated CSVs into {}".format(consolidated_filepath))
115
+
116
+ return consolidated_filepath
117
+
118
+ def operate_winscp(operation_type, files, endpoint_config, local_storage_path, config):
119
+ """
120
+ General function to operate WinSCP for uploading or downloading files.
121
+
122
+ :param operation_type: 'upload' or 'download'
123
+ :param files: List of files to upload or pattern for files to download.
124
+ :param endpoint_config: Dictionary containing endpoint configuration.
125
+ :param local_storage_path: Base local storage path for logs and files.
126
+
127
+ # Example of how to call this function for uploads
128
+ upload_files = ['path/to/local/file1.txt', 'path/to/local/file2.txt']
129
+ upload_config = {
130
+ 'session_name': 'MySession',
131
+ 'remote_directory_up': '/remote/upload/path'
132
+ }
133
+
134
+ operate_winscp('upload', upload_files, upload_config, 'path/to/local/storage', config)
135
+
136
+ # Example of how to call this function for downloads
137
+ download_config = {
138
+ 'session_name': 'MySession',
139
+ 'remote_directory_down': '/remote/download/path'
140
+ }
141
+
142
+ operate_winscp('download', None, download_config, 'path/to/local/storage', config)
143
+ """
144
+ # Setup paths
145
+ try:
146
+ # TODO (Easy / Config) Get this updated. ??
147
+ winscp_path = config['winscp_path']
148
+ except KeyError:
149
+ winscp_path = os.path.join(os.getcwd(), "Installers", "WinSCP-Portable", "WinSCP.com")
150
+ except Exception as e:
151
+ # Handle any other exceptions here
152
+ print("An error occurred while running WinSCP:", e)
153
+ winscp_path = None
154
+
155
+ if not os.path.isfile(winscp_path):
156
+ MediLink_ConfigLoader.log("WinSCP.com not found at {}".format(winscp_path))
157
+ return []
158
+
159
+ # Setup logging
160
+ log_filename = "winscp_upload.log" if operation_type == "upload" else "winscp_download.log"
161
+ winscp_log_path = os.path.join(local_storage_path, log_filename)
162
+
163
+ # Session and directory setup
164
+ session_name = endpoint_config.get('session_name', '')
165
+ remote_directory = endpoint_config['remote_directory_up'] if operation_type == "upload" else endpoint_config['remote_directory_down']
166
+
167
+ # Command building
168
+ command = [
169
+ winscp_path,
170
+ '/log=' + winscp_log_path,
171
+ '/loglevel=1',
172
+ '/command',
173
+ 'open {}'.format(session_name),
174
+ 'cd /',
175
+ 'cd {}'.format(remote_directory)
176
+ ]
177
+
178
+ # Add commands to WinSCP script
179
+ # BUG (Low SFTP) We really need to fix this path situation.
180
+ # Unfortunately, this just needs to be a non-spaced path because WinSCP can't
181
+ # handle the spaces. Also, Windows won't let me use shutil to move the files out of G:\ into C:\ and it it wants an administrator security
182
+ # check or verification thing for me to even move the file by hand so that doesn't work either.
183
+ # command.append("put {}".format("C:\\Z_optumedi_04161742.txt"))
184
+ if operation_type == "upload":
185
+ for file_path in files:
186
+ normalized_path = os.path.normpath(file_path)
187
+ command.append("put {}".format(normalized_path))
188
+ else:
189
+ command.append('get *') # Adjust pattern as needed
190
+
191
+ command += ['close', 'exit']
192
+
193
+ # Check if TestMode is enabled in the configuration
194
+ if config.get("MediLink_Config", {}).get("TestMode", True):
195
+ # TestMode is enabled, do not execute the command
196
+ print("Test Mode is enabled! WinSCP Command not executed.")
197
+ MediLink_ConfigLoader.log("Test Mode is enabled! WinSCP Command not executed.")
198
+ MediLink_ConfigLoader.log("TEST MODE: Simulating WinSCP Upload File List.")
199
+ uploaded_files = []
200
+ for file_path in files:
201
+ normalized_path = os.path.normpath(file_path)
202
+ if os.path.exists(normalized_path): # Check if the file exists before appending
203
+ uploaded_files.append(normalized_path)
204
+ else:
205
+ MediLink_ConfigLoader.log("TEST MODE: Failed to upload file: {} does not exist.".format(normalized_path))
206
+ return uploaded_files
207
+ else:
208
+ # TestMode is not enabled, execute the command
209
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
210
+ stdout, stderr = process.communicate()
211
+
212
+ if process.returncode == 0: # BUG Does this work as intended?
213
+ MediLink_ConfigLoader.log("WinSCP {} attempted.".format(operation_type))
214
+ # Construct a list of downloaded files if operation_type is 'download'
215
+ if operation_type == 'download':
216
+ downloaded_files = []
217
+ for root, dirs, files in os.walk(local_storage_path):
218
+ for file in files:
219
+ downloaded_files.append(os.path.join(root, file))
220
+ return downloaded_files
221
+
222
+ if operation_type == 'upload':
223
+ # Return a list of uploaded files
224
+ uploaded_files = []
225
+ for file_path in files:
226
+ normalized_path = os.path.normpath(file_path)
227
+ if os.path.exists(normalized_path): # Check if the file exists before appending
228
+ uploaded_files.append(normalized_path)
229
+ else:
230
+ MediLink_ConfigLoader.log("Failed to upload file: {} does not exist.".format(normalized_path))
231
+ return uploaded_files
232
+ else:
233
+ MediLink_ConfigLoader.log("Failed to {} files. Details: {}".format(operation_type, stderr.decode('utf-8')))
234
+ return [] # Return empty list to indicate failure. BUG check to make sure this doesn't break something else.
235
+
236
+ # UNUSED CSV Functions
237
+ """
238
+ def remove_blank_rows_from_csv(csv_file_path):
239
+ with open(csv_file_path, 'r') as csv_file:
240
+ # Read the CSV file and filter out any empty rows
241
+ rows = [row for row in csv.reader(csv_file) if any(field.strip() for field in row)]
242
+
243
+ # Write the filtered rows back to the CSV file
244
+ with open(csv_file_path, 'w', newline='') as csv_file:
245
+ writer = csv.writer(csv_file)
246
+ writer.writerows(rows)
247
+
248
+ def list_chart_numbers_in_existing_file(filepath):
249
+ # Lists the Chart Numbers contained in an existing CSV file.
250
+ chart_numbers = []
251
+ with open(filepath, 'r', newline='') as csvfile:
252
+ reader = csv.reader(csvfile)
253
+ next(reader) # Skip header
254
+ for row in reader:
255
+ if len(row) > 2: # Assuming Chart Number is in the 3rd column
256
+ chart_numbers.append(row[2])
257
+ return chart_numbers
258
+ """