medicafe 0.240419.2__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.
- MediBot/MediBot.bat +166 -38
- MediBot/MediBot.py +74 -44
- MediBot/MediBot_Crosswalk_Library.py +280 -0
- MediBot/MediBot_Preprocessor.py +155 -191
- MediBot/MediBot_Preprocessor_lib.py +357 -0
- MediBot/MediBot_UI.py +80 -30
- MediBot/MediBot_dataformat_library.py +88 -35
- MediBot/MediBot_docx_decoder.py +80 -0
- MediBot/update_medicafe.py +46 -8
- MediLink/MediLink.py +138 -34
- MediLink/MediLink_837p_encoder.py +319 -209
- MediLink/MediLink_837p_encoder_library.py +453 -242
- MediLink/MediLink_API_v2.py +174 -0
- MediLink/MediLink_APIs.py +137 -0
- MediLink/MediLink_ConfigLoader.py +44 -32
- MediLink/MediLink_DataMgmt.py +85 -33
- MediLink/MediLink_Down.py +12 -35
- MediLink/MediLink_ERA_decoder.py +4 -4
- MediLink/MediLink_Gmail.py +99 -3
- MediLink/MediLink_Mailer.py +7 -0
- MediLink/MediLink_Scheduler.py +41 -0
- MediLink/MediLink_UI.py +19 -17
- MediLink/MediLink_Up.py +297 -31
- MediLink/MediLink_batch.bat +1 -1
- MediLink/test.py +74 -0
- medicafe-0.240517.0.dist-info/METADATA +53 -0
- medicafe-0.240517.0.dist-info/RECORD +39 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/WHEEL +5 -5
- medicafe-0.240419.2.dist-info/METADATA +0 -19
- medicafe-0.240419.2.dist-info/RECORD +0 -32
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240517.0.dist-info}/top_level.txt +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)))
|
|
@@ -4,45 +4,28 @@ import logging
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from collections import OrderedDict
|
|
6
6
|
import sys
|
|
7
|
+
import platform
|
|
7
8
|
|
|
8
9
|
"""
|
|
9
10
|
This function should be generalizable to have a initialization script over all the Medi* functions
|
|
10
11
|
"""
|
|
11
|
-
|
|
12
|
-
# Setup basic logging.
|
|
13
|
-
# BUG Consolidate this with MediLink_837p_encoder_library.log
|
|
14
|
-
def setup_logger(local_storage_path):
|
|
15
|
-
# Define a reasonable name for the log file, e.g., "MediLink_Down_Process.log"
|
|
16
|
-
log_filename = datetime.now().strftime("MediLink_Down_Process_%m%d%Y.log")
|
|
17
|
-
log_filepath = os.path.join(local_storage_path, log_filename)
|
|
18
|
-
|
|
19
|
-
for handler in logging.root.handlers[:]:
|
|
20
|
-
logging.root.removeHandler(handler)
|
|
21
|
-
|
|
22
|
-
# Setup logging to file
|
|
23
|
-
logging.basicConfig(level=logging.INFO,
|
|
24
|
-
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
25
|
-
filename=log_filepath, # Direct logging to a file in local_storage_path
|
|
26
|
-
filemode='a') # Append mode
|
|
27
|
-
|
|
28
|
-
# If you also want to see the logs in the console, add a StreamHandler
|
|
29
|
-
#console_handler = logging.StreamHandler()
|
|
30
|
-
#console_handler.setLevel(logging.INFO)
|
|
31
|
-
#formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
32
|
-
#console_handler.setFormatter(formatter)
|
|
33
|
-
#logging.getLogger('').addHandler(console_handler)
|
|
34
|
-
|
|
35
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')):
|
|
36
13
|
"""
|
|
37
14
|
Loads endpoint configuration, credentials, and other settings from JSON files.
|
|
38
15
|
|
|
39
16
|
Returns: A tuple containing dictionaries with configuration settings for the main config and crosswalk.
|
|
40
17
|
"""
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
-
|
|
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"
|
|
46
29
|
|
|
47
30
|
try:
|
|
48
31
|
with open(config_path, 'r') as config_file:
|
|
@@ -55,8 +38,11 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
|
55
38
|
crosswalk = json.load(crosswalk_file)
|
|
56
39
|
|
|
57
40
|
return config, crosswalk
|
|
58
|
-
except
|
|
59
|
-
|
|
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))
|
|
60
46
|
sys.exit(1) # Exit the script due to a critical error in configuration loading
|
|
61
47
|
except FileNotFoundError:
|
|
62
48
|
print("One or both JSON files not found. Config: {}, Crosswalk: {}".format(config_path, crosswalk_path))
|
|
@@ -66,4 +52,30 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
|
66
52
|
sys.exit(1) # Exit the script due to a critical error in configuration loading
|
|
67
53
|
except Exception as e:
|
|
68
54
|
print("An unexpected error occurred while loading the configuration: {}".format(e))
|
|
69
|
-
sys.exit(1) # Exit the script due to a critical error in configuration loading
|
|
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)
|
MediLink/MediLink_DataMgmt.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import csv
|
|
2
2
|
import os
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
8
11
|
|
|
9
12
|
# Helper function to slice and strip values
|
|
10
13
|
def slice_data(data, slices):
|
|
@@ -12,7 +15,15 @@ def slice_data(data, slices):
|
|
|
12
15
|
return {key: data[slice(*slices[key])].strip() for key in slices}
|
|
13
16
|
|
|
14
17
|
# Function to parse fixed-width Medisoft output and extract claim data
|
|
15
|
-
def parse_fixed_width_data(personal_info, insurance_info, service_info, config):
|
|
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
|
+
|
|
16
27
|
# Load slice definitions from config within the MediLink_Config section
|
|
17
28
|
personal_slices = config['fixedWidthSlices']['personal_slices']
|
|
18
29
|
insurance_slices = config['fixedWidthSlices']['insurance_slices']
|
|
@@ -24,14 +35,15 @@ def parse_fixed_width_data(personal_info, insurance_info, service_info, config):
|
|
|
24
35
|
parsed_data.update(slice_data(insurance_info, insurance_slices))
|
|
25
36
|
parsed_data.update(slice_data(service_info, service_slices))
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
MediLink_ConfigLoader.log("Successfully parsed data from segments", config, level="INFO")
|
|
28
39
|
|
|
29
40
|
return parsed_data
|
|
30
41
|
|
|
31
42
|
# Function to read fixed-width Medisoft output and extract claim data
|
|
32
|
-
def read_fixed_width_data(file_path
|
|
43
|
+
def read_fixed_width_data(file_path):
|
|
33
44
|
# Reads the fixed width data from the file and yields each patient's
|
|
34
45
|
# personal, insurance, and service information.
|
|
46
|
+
MediLink_ConfigLoader.log("Starting to read fixed width data...")
|
|
35
47
|
with open(file_path, 'r') as file:
|
|
36
48
|
lines_buffer = [] # Buffer to hold lines for current patient data
|
|
37
49
|
for line in file:
|
|
@@ -41,12 +53,21 @@ def read_fixed_width_data(file_path, config):
|
|
|
41
53
|
# Once we have 3 lines of data, yield them as a patient record
|
|
42
54
|
if len(lines_buffer) == 3:
|
|
43
55
|
personal_info, insurance_info, service_info = lines_buffer
|
|
44
|
-
|
|
56
|
+
MediLink_ConfigLoader.log("Successfully read data from file: {}".format(file_path), level="INFO")
|
|
45
57
|
yield personal_info, insurance_info, service_info
|
|
46
58
|
lines_buffer.clear() # Reset buffer for the next patient record
|
|
47
59
|
# If the line is blank but we have already started collecting a patient record,
|
|
48
60
|
# we continue without resetting the buffer, effectively skipping blank lines.
|
|
49
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
|
+
|
|
50
71
|
def consolidate_csvs(source_directory):
|
|
51
72
|
"""
|
|
52
73
|
This default overwrites any existing CSV for the same day. We want this for the automated runs but want to switch through
|
|
@@ -61,7 +82,7 @@ def consolidate_csvs(source_directory):
|
|
|
61
82
|
|
|
62
83
|
# Check if the file already exists and log the action
|
|
63
84
|
if os.path.exists(consolidated_filepath):
|
|
64
|
-
|
|
85
|
+
MediLink_ConfigLoader.log("The file {} already exists. It will be overwritten.".format(consolidated_filename))
|
|
65
86
|
|
|
66
87
|
for filename in os.listdir(source_directory):
|
|
67
88
|
filepath = os.path.join(source_directory, filename)
|
|
@@ -90,11 +111,11 @@ def consolidate_csvs(source_directory):
|
|
|
90
111
|
writer = csv.writer(csvfile)
|
|
91
112
|
writer.writerows(consolidated_data)
|
|
92
113
|
|
|
93
|
-
|
|
114
|
+
MediLink_ConfigLoader.log("Consolidated CSVs into {}".format(consolidated_filepath))
|
|
94
115
|
|
|
95
116
|
return consolidated_filepath
|
|
96
117
|
|
|
97
|
-
def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
118
|
+
def operate_winscp(operation_type, files, endpoint_config, local_storage_path, config):
|
|
98
119
|
"""
|
|
99
120
|
General function to operate WinSCP for uploading or downloading files.
|
|
100
121
|
|
|
@@ -110,7 +131,7 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
110
131
|
'remote_directory_up': '/remote/upload/path'
|
|
111
132
|
}
|
|
112
133
|
|
|
113
|
-
operate_winscp('upload', upload_files, upload_config, 'path/to/local/storage')
|
|
134
|
+
operate_winscp('upload', upload_files, upload_config, 'path/to/local/storage', config)
|
|
114
135
|
|
|
115
136
|
# Example of how to call this function for downloads
|
|
116
137
|
download_config = {
|
|
@@ -118,21 +139,22 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
118
139
|
'remote_directory_down': '/remote/download/path'
|
|
119
140
|
}
|
|
120
141
|
|
|
121
|
-
operate_winscp('download', None, download_config, 'path/to/local/storage')
|
|
142
|
+
operate_winscp('download', None, download_config, 'path/to/local/storage', config)
|
|
122
143
|
"""
|
|
123
144
|
# Setup paths
|
|
124
145
|
try:
|
|
125
|
-
|
|
146
|
+
# TODO (Easy / Config) Get this updated. ??
|
|
147
|
+
winscp_path = config['winscp_path']
|
|
126
148
|
except KeyError:
|
|
127
149
|
winscp_path = os.path.join(os.getcwd(), "Installers", "WinSCP-Portable", "WinSCP.com")
|
|
128
150
|
except Exception as e:
|
|
129
151
|
# Handle any other exceptions here
|
|
130
|
-
print("An error occurred:", e)
|
|
152
|
+
print("An error occurred while running WinSCP:", e)
|
|
131
153
|
winscp_path = None
|
|
132
154
|
|
|
133
155
|
if not os.path.isfile(winscp_path):
|
|
134
|
-
|
|
135
|
-
return
|
|
156
|
+
MediLink_ConfigLoader.log("WinSCP.com not found at {}".format(winscp_path))
|
|
157
|
+
return []
|
|
136
158
|
|
|
137
159
|
# Setup logging
|
|
138
160
|
log_filename = "winscp_upload.log" if operation_type == "upload" else "winscp_download.log"
|
|
@@ -154,7 +176,7 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
154
176
|
]
|
|
155
177
|
|
|
156
178
|
# Add commands to WinSCP script
|
|
157
|
-
# BUG We really need to fix this path situation.
|
|
179
|
+
# BUG (Low SFTP) We really need to fix this path situation.
|
|
158
180
|
# Unfortunately, this just needs to be a non-spaced path because WinSCP can't
|
|
159
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
|
|
160
182
|
# check or verification thing for me to even move the file by hand so that doesn't work either.
|
|
@@ -162,28 +184,57 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
162
184
|
if operation_type == "upload":
|
|
163
185
|
for file_path in files:
|
|
164
186
|
normalized_path = os.path.normpath(file_path)
|
|
165
|
-
command.append("put
|
|
187
|
+
command.append("put {}".format(normalized_path))
|
|
166
188
|
else:
|
|
167
189
|
command.append('get *') # Adjust pattern as needed
|
|
168
190
|
|
|
169
191
|
command += ['close', 'exit']
|
|
170
192
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
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()
|
|
174
211
|
|
|
175
|
-
if
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
182
232
|
else:
|
|
183
|
-
|
|
184
|
-
return
|
|
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.
|
|
185
235
|
|
|
186
236
|
# UNUSED CSV Functions
|
|
237
|
+
"""
|
|
187
238
|
def remove_blank_rows_from_csv(csv_file_path):
|
|
188
239
|
with open(csv_file_path, 'r') as csv_file:
|
|
189
240
|
# Read the CSV file and filter out any empty rows
|
|
@@ -195,7 +246,7 @@ def remove_blank_rows_from_csv(csv_file_path):
|
|
|
195
246
|
writer.writerows(rows)
|
|
196
247
|
|
|
197
248
|
def list_chart_numbers_in_existing_file(filepath):
|
|
198
|
-
|
|
249
|
+
# Lists the Chart Numbers contained in an existing CSV file.
|
|
199
250
|
chart_numbers = []
|
|
200
251
|
with open(filepath, 'r', newline='') as csvfile:
|
|
201
252
|
reader = csv.reader(csvfile)
|
|
@@ -203,4 +254,5 @@ def list_chart_numbers_in_existing_file(filepath):
|
|
|
203
254
|
for row in reader:
|
|
204
255
|
if len(row) > 2: # Assuming Chart Number is in the 3rd column
|
|
205
256
|
chart_numbers.append(row[2])
|
|
206
|
-
return chart_numbers
|
|
257
|
+
return chart_numbers
|
|
258
|
+
"""
|