medicafe 0.250810.7__py3-none-any.whl → 0.250811.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.
- MediBot/MediBot_UI.py +35 -7
- MediCafe/MediLink_ConfigLoader.py +32 -15
- MediCafe/core_utils.py +157 -6
- MediLink/MediLink_Gmail.py +57 -308
- MediLink/gmail_http_utils.py +88 -0
- MediLink/gmail_oauth_utils.py +98 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.0.dist-info}/METADATA +1 -1
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.0.dist-info}/RECORD +12 -10
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.0.dist-info}/LICENSE +0 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.0.dist-info}/WHEEL +0 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.0.dist-info}/entry_points.txt +0 -0
- {medicafe-0.250810.7.dist-info → medicafe-0.250811.0.dist-info}/top_level.txt +0 -0
MediBot/MediBot_UI.py
CHANGED
@@ -45,7 +45,13 @@ class AppControl:
|
|
45
45
|
# PERFORMANCE FIX: Add configuration caching to reduce lookup overhead
|
46
46
|
self._config_cache = {} # Cache for Medicare vs Private configuration lookups
|
47
47
|
# Load initial paths from config when instance is created
|
48
|
-
|
48
|
+
try:
|
49
|
+
self.load_paths_from_config()
|
50
|
+
except Exception:
|
51
|
+
# Defer configuration loading until first access if config is unavailable
|
52
|
+
self._deferred_load = True
|
53
|
+
else:
|
54
|
+
self._deferred_load = False
|
49
55
|
|
50
56
|
def get_pause_status(self):
|
51
57
|
return self.script_paused
|
@@ -91,8 +97,29 @@ class AppControl:
|
|
91
97
|
self.mapat_med_path = cached['mapat_path']
|
92
98
|
self.medisoft_shortcut = cached['shortcut']
|
93
99
|
|
94
|
-
|
95
|
-
|
100
|
+
def _get_app_control():
|
101
|
+
global app_control
|
102
|
+
try:
|
103
|
+
ac = app_control
|
104
|
+
except NameError:
|
105
|
+
ac = None
|
106
|
+
if ac is None:
|
107
|
+
ac = AppControl()
|
108
|
+
# If deferred, attempt first load now
|
109
|
+
try:
|
110
|
+
if getattr(ac, '_deferred_load', False):
|
111
|
+
ac.load_paths_from_config()
|
112
|
+
ac._deferred_load = False
|
113
|
+
except Exception:
|
114
|
+
pass
|
115
|
+
globals()['app_control'] = ac
|
116
|
+
return ac
|
117
|
+
|
118
|
+
# Lazily initialize app_control to avoid config load at import time
|
119
|
+
try:
|
120
|
+
app_control
|
121
|
+
except NameError:
|
122
|
+
app_control = None
|
96
123
|
|
97
124
|
|
98
125
|
def is_key_pressed(key_code):
|
@@ -105,15 +132,16 @@ def manage_script_pause(csv_data, error_message, reverse_mapping):
|
|
105
132
|
user_action = 0 # initialize as 'continue'
|
106
133
|
VK_END, VK_PAUSE = _get_vk_codes()
|
107
134
|
|
108
|
-
|
109
|
-
|
135
|
+
ac = _get_app_control()
|
136
|
+
if not ac.get_pause_status() and is_key_pressed(VK_PAUSE):
|
137
|
+
ac.set_pause_status(True)
|
110
138
|
print("Script paused. Opening menu...")
|
111
139
|
interaction_mode = 'normal' # Assuming normal interaction mode for script pause
|
112
140
|
user_action = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
113
141
|
|
114
|
-
while
|
142
|
+
while ac.get_pause_status():
|
115
143
|
if is_key_pressed(VK_END):
|
116
|
-
|
144
|
+
ac.set_pause_status(False)
|
117
145
|
print("Continuing...")
|
118
146
|
elif is_key_pressed(VK_PAUSE):
|
119
147
|
user_action = user_interaction(csv_data, 'normal', error_message, reverse_mapping)
|
@@ -105,13 +105,13 @@ def load_configuration(config_path=os.path.join(os.path.dirname(__file__), '..',
|
|
105
105
|
sys.exit(1) # Exit the script due to a critical error in configuration loading
|
106
106
|
except FileNotFoundError:
|
107
107
|
print("One or both configuration files not found. Config: {}, Crosswalk: {}".format(config_path, crosswalk_path))
|
108
|
-
|
108
|
+
raise
|
109
109
|
except KeyError as e:
|
110
110
|
print("Critical configuration is missing: {}".format(e))
|
111
|
-
|
111
|
+
raise
|
112
112
|
except Exception as e:
|
113
113
|
print("An unexpected error occurred while loading the configuration: {}".format(e))
|
114
|
-
|
114
|
+
raise
|
115
115
|
|
116
116
|
def clear_config_cache():
|
117
117
|
"""Clear the configuration cache to force reloading on next call."""
|
@@ -124,14 +124,23 @@ def log(message, config=None, level="INFO", error_type=None, claim=None, verbose
|
|
124
124
|
|
125
125
|
# If config is not provided, use cached config or load it
|
126
126
|
if config is None:
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
127
|
+
try:
|
128
|
+
if _CONFIG_CACHE is None:
|
129
|
+
config, _ = load_configuration()
|
130
|
+
else:
|
131
|
+
config = _CONFIG_CACHE
|
132
|
+
except BaseException:
|
133
|
+
# Configuration unavailable; fall back to minimal console logging
|
134
|
+
config = {}
|
131
135
|
|
132
136
|
# Setup logger if not already configured
|
133
137
|
if not logging.root.handlers:
|
134
|
-
local_storage_path =
|
138
|
+
local_storage_path = '.'
|
139
|
+
if isinstance(config, dict):
|
140
|
+
try:
|
141
|
+
local_storage_path = config.get('MediLink_Config', {}).get('local_storage_path', '.')
|
142
|
+
except Exception:
|
143
|
+
local_storage_path = '.'
|
135
144
|
log_filename = datetime.now().strftime("Log_%m%d%Y.log")
|
136
145
|
log_filepath = os.path.join(local_storage_path, log_filename)
|
137
146
|
|
@@ -141,12 +150,15 @@ def log(message, config=None, level="INFO", error_type=None, claim=None, verbose
|
|
141
150
|
# Create formatter
|
142
151
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
143
152
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
153
|
+
handlers = []
|
154
|
+
try:
|
155
|
+
# Create file handler when path is usable
|
156
|
+
file_handler = logging.FileHandler(log_filepath, mode='a')
|
157
|
+
file_handler.setFormatter(formatter)
|
158
|
+
handlers.append(file_handler)
|
159
|
+
except Exception:
|
160
|
+
# Fall back to console-only if file handler cannot be created
|
161
|
+
pass
|
150
162
|
|
151
163
|
# Add console handler only if console_output is True
|
152
164
|
if console_output:
|
@@ -154,7 +166,12 @@ def log(message, config=None, level="INFO", error_type=None, claim=None, verbose
|
|
154
166
|
console_handler.setFormatter(formatter)
|
155
167
|
handlers.append(console_handler)
|
156
168
|
|
157
|
-
#
|
169
|
+
# If no handlers could be added (e.g., file path invalid and console_output False), add a console handler
|
170
|
+
if not handlers:
|
171
|
+
console_handler = logging.StreamHandler()
|
172
|
+
console_handler.setFormatter(formatter)
|
173
|
+
handlers.append(console_handler)
|
174
|
+
|
158
175
|
logging.basicConfig(level=logging_level, handlers=handlers)
|
159
176
|
|
160
177
|
# Prepare log message
|
MediCafe/core_utils.py
CHANGED
@@ -513,16 +513,17 @@ def get_api_client_factory():
|
|
513
513
|
config_loader = get_shared_config_loader()
|
514
514
|
if config_loader:
|
515
515
|
try:
|
516
|
+
# Be resilient to SystemExit raised inside loaders
|
516
517
|
config, _ = config_loader.load_configuration()
|
517
|
-
factory_config = config.get('API_Factory_Config', {})
|
518
|
+
factory_config = config.get('API_Factory_Config', {}) if isinstance(config, dict) else {}
|
518
519
|
return APIClientFactory(factory_config)
|
519
|
-
except
|
520
|
-
# Fall back to default configuration
|
520
|
+
except BaseException:
|
521
|
+
# Fall back to default configuration on any loader failure (including SystemExit)
|
521
522
|
return APIClientFactory()
|
522
523
|
else:
|
523
524
|
return APIClientFactory()
|
524
|
-
except
|
525
|
-
#
|
525
|
+
except BaseException:
|
526
|
+
# Do not allow API client factory acquisition to crash callers during import time
|
526
527
|
return None
|
527
528
|
|
528
529
|
def get_api_client(**kwargs):
|
@@ -555,4 +556,154 @@ def get_api_core_client(**kwargs):
|
|
555
556
|
return APIClient(**kwargs)
|
556
557
|
except ImportError:
|
557
558
|
# Don't log error here - just return None silently
|
558
|
-
return None
|
559
|
+
return None
|
560
|
+
|
561
|
+
# --- Compatibility & Process Utilities (Python 3.4.4 / Windows XP friendly) ---
|
562
|
+
|
563
|
+
def is_python_34_compatible(version_info=None):
|
564
|
+
"""
|
565
|
+
Return True if the interpreter is Python 3.4.4+ (as required in XP env).
|
566
|
+
This consolidates scattered version checks into a single function so
|
567
|
+
call sites remain concise and intention-revealing.
|
568
|
+
"""
|
569
|
+
try:
|
570
|
+
if version_info is None:
|
571
|
+
version_info = sys.version_info
|
572
|
+
major = getattr(version_info, 'major', version_info[0])
|
573
|
+
minor = getattr(version_info, 'minor', version_info[1])
|
574
|
+
micro = getattr(version_info, 'micro', version_info[2])
|
575
|
+
return (major, minor, micro) >= (3, 4, 4)
|
576
|
+
except Exception:
|
577
|
+
# Be conservative if we cannot determine
|
578
|
+
return False
|
579
|
+
|
580
|
+
def format_py34(template, *args, **kwargs):
|
581
|
+
"""
|
582
|
+
Safe stand-in for f-strings (not available in Python 3.4).
|
583
|
+
Centralized to avoid ad-hoc "format_string" helpers scattered around.
|
584
|
+
"""
|
585
|
+
try:
|
586
|
+
return template.format(*args, **kwargs)
|
587
|
+
except Exception:
|
588
|
+
# If formatting fails, return template unmodified to avoid crashes
|
589
|
+
return template
|
590
|
+
|
591
|
+
def _decode_bytes(data, encoding_list=None):
|
592
|
+
"""
|
593
|
+
Decode bytes to text using a tolerant strategy.
|
594
|
+
We avoid relying on platform defaults (XP may vary) and try multiple
|
595
|
+
encodings to reduce boilerplate at call sites.
|
596
|
+
"""
|
597
|
+
if data is None:
|
598
|
+
return ''
|
599
|
+
if isinstance(data, str):
|
600
|
+
return data
|
601
|
+
if encoding_list is None:
|
602
|
+
encoding_list = ['utf-8', 'latin-1', 'ascii']
|
603
|
+
for enc in encoding_list:
|
604
|
+
try:
|
605
|
+
return data.decode(enc, 'ignore')
|
606
|
+
except Exception:
|
607
|
+
continue
|
608
|
+
# Last resort: str() on bytes
|
609
|
+
try:
|
610
|
+
return str(data)
|
611
|
+
except Exception:
|
612
|
+
return ''
|
613
|
+
|
614
|
+
def run_cmd(cmd, timeout=None, input_text=None, shell=False, cwd=None, env=None):
|
615
|
+
"""
|
616
|
+
Cross-version subprocess runner for Python 3.4/XP environments.
|
617
|
+
- Uses subprocess.Popen (subprocess.run is 3.5+)
|
618
|
+
- Returns (returncode, stdout_text, stderr_text)
|
619
|
+
- Accepts optional timeout (best-effort with polling for 3.4)
|
620
|
+
- Accepts optional input_text (string) which will be encoded to bytes
|
621
|
+
This consolidates repetitive Popen/communicate/decode blocks.
|
622
|
+
"""
|
623
|
+
import subprocess
|
624
|
+
try:
|
625
|
+
# Prepare stdin if input is provided
|
626
|
+
stdin_pipe = subprocess.PIPE if input_text is not None else None
|
627
|
+
p = subprocess.Popen(
|
628
|
+
cmd,
|
629
|
+
stdin=stdin_pipe,
|
630
|
+
stdout=subprocess.PIPE,
|
631
|
+
stderr=subprocess.PIPE,
|
632
|
+
shell=shell,
|
633
|
+
cwd=cwd,
|
634
|
+
env=env
|
635
|
+
)
|
636
|
+
if timeout is None:
|
637
|
+
out_bytes, err_bytes = p.communicate(
|
638
|
+
input=input_text.encode('utf-8') if isinstance(input_text, str) else input_text
|
639
|
+
)
|
640
|
+
return p.returncode, _decode_bytes(out_bytes), _decode_bytes(err_bytes)
|
641
|
+
# Implement simple timeout loop compatible with 3.4
|
642
|
+
import time
|
643
|
+
start_time = time.time()
|
644
|
+
while True:
|
645
|
+
if p.poll() is not None:
|
646
|
+
out_bytes, err_bytes = p.communicate()
|
647
|
+
return p.returncode, _decode_bytes(out_bytes), _decode_bytes(err_bytes)
|
648
|
+
if time.time() - start_time > timeout:
|
649
|
+
try:
|
650
|
+
p.kill()
|
651
|
+
except Exception:
|
652
|
+
try:
|
653
|
+
p.terminate()
|
654
|
+
except Exception:
|
655
|
+
pass
|
656
|
+
out_bytes, err_bytes = p.communicate()
|
657
|
+
return 124, _decode_bytes(out_bytes), _decode_bytes(err_bytes)
|
658
|
+
time.sleep(0.05)
|
659
|
+
except Exception as e:
|
660
|
+
# Standardize failure surface
|
661
|
+
return 1, '', str(e)
|
662
|
+
|
663
|
+
def file_is_ascii(file_path):
|
664
|
+
"""
|
665
|
+
Return True if the given file can be read as ASCII-only.
|
666
|
+
Consolidates repeated try/open/decode blocks into one helper.
|
667
|
+
"""
|
668
|
+
try:
|
669
|
+
if not os.path.exists(file_path):
|
670
|
+
return False
|
671
|
+
# Use strict ASCII to detect any non-ASCII characters
|
672
|
+
f = open(file_path, 'r')
|
673
|
+
try:
|
674
|
+
# Explicit codec arg not supported in some XP Python builds for text mode
|
675
|
+
# so we emulate by checking ordinals while reading
|
676
|
+
data = f.read()
|
677
|
+
finally:
|
678
|
+
try:
|
679
|
+
f.close()
|
680
|
+
except Exception:
|
681
|
+
pass
|
682
|
+
try:
|
683
|
+
data.encode('ascii')
|
684
|
+
return True
|
685
|
+
except Exception:
|
686
|
+
return False
|
687
|
+
except Exception:
|
688
|
+
return False
|
689
|
+
|
690
|
+
def check_ascii_files(paths):
|
691
|
+
"""
|
692
|
+
Given a list of paths (absolute or relative), return a list of those that
|
693
|
+
contain non-ASCII characters or could not be checked. This replaces
|
694
|
+
duplicated loops in various verification scripts.
|
695
|
+
"""
|
696
|
+
problematic = []
|
697
|
+
try:
|
698
|
+
for p in paths:
|
699
|
+
# Normalize relative to current working directory for consistency
|
700
|
+
if not os.path.isabs(p):
|
701
|
+
abs_p = os.path.abspath(p)
|
702
|
+
else:
|
703
|
+
abs_p = p
|
704
|
+
if not file_is_ascii(abs_p):
|
705
|
+
problematic.append(p)
|
706
|
+
except Exception:
|
707
|
+
# If a failure occurs mid-scan, return what we have so far
|
708
|
+
return problematic
|
709
|
+
return problematic
|
MediLink/MediLink_Gmail.py
CHANGED
@@ -16,6 +16,20 @@ setup_python_path()
|
|
16
16
|
|
17
17
|
from MediCafe.core_utils import get_shared_config_loader
|
18
18
|
|
19
|
+
# New helpers
|
20
|
+
from MediLink.gmail_oauth_utils import (
|
21
|
+
get_authorization_url as oauth_get_authorization_url,
|
22
|
+
exchange_code_for_token as oauth_exchange_code_for_token,
|
23
|
+
refresh_access_token as oauth_refresh_access_token,
|
24
|
+
is_valid_authorization_code as oauth_is_valid_authorization_code,
|
25
|
+
clear_token_cache as oauth_clear_token_cache,
|
26
|
+
)
|
27
|
+
from MediLink.gmail_http_utils import (
|
28
|
+
generate_self_signed_cert as http_generate_self_signed_cert,
|
29
|
+
start_https_server as http_start_https_server,
|
30
|
+
inspect_token as http_inspect_token,
|
31
|
+
)
|
32
|
+
|
19
33
|
# Get shared config loader
|
20
34
|
MediLink_ConfigLoader = get_shared_config_loader()
|
21
35
|
if MediLink_ConfigLoader:
|
@@ -94,77 +108,29 @@ log("Using CREDENTIALS_PATH: {}".format(CREDENTIALS_PATH), config, level="INFO")
|
|
94
108
|
REDIRECT_URI = 'https://127.0.0.1:8000'
|
95
109
|
|
96
110
|
def get_authorization_url():
|
97
|
-
|
98
|
-
credentials = json.load(credentials_file)
|
99
|
-
client_id = credentials['web']['client_id']
|
100
|
-
auth_url = (
|
101
|
-
"https://accounts.google.com/o/oauth2/v2/auth?"
|
102
|
-
"response_type=code&"
|
103
|
-
"client_id={}&"
|
104
|
-
"redirect_uri={}&"
|
105
|
-
"scope={}&"
|
106
|
-
"access_type=offline&" # Requesting offline access allows the application to obtain a refresh token, enabling it to access resources even when the user is not actively using the app. This is useful for long-lived sessions.
|
107
|
-
# To improve user experience, consider changing this to 'online' if you don't need offline access:
|
108
|
-
# "access_type=online&" # Use this if you only need access while the user is actively using the app and don't require a refresh token.
|
109
|
-
|
110
|
-
"prompt=consent" # This forces the user to re-consent to the requested scopes every time they authenticate. While this is useful for ensuring the user is aware of the permissions being granted, it can be modified to 'none' or omitted entirely if the application is functioning correctly and tokens are being refreshed properly.
|
111
|
-
# To improve user experience, consider changing this to 'none' if you want to avoid showing the consent screen every time:
|
112
|
-
# "prompt=none" # Use this if you want to skip the consent screen for users who have already granted permissions.
|
113
|
-
# Alternatively, you can omit the prompt parameter entirely to use the default behavior:
|
114
|
-
# # "prompt=" # Omitting this will show the consent screen only when necessary.
|
115
|
-
).format(client_id, REDIRECT_URI, SCOPES)
|
116
|
-
log("Generated authorization URL: {}".format(auth_url))
|
117
|
-
return auth_url
|
111
|
+
return oauth_get_authorization_url(CREDENTIALS_PATH, REDIRECT_URI, SCOPES, log)
|
118
112
|
|
119
113
|
def exchange_code_for_token(auth_code, retries=3):
|
120
|
-
|
121
|
-
try:
|
122
|
-
with open(CREDENTIALS_PATH, 'r') as credentials_file:
|
123
|
-
credentials = json.load(credentials_file)
|
124
|
-
token_url = "https://oauth2.googleapis.com/token"
|
125
|
-
data = {
|
126
|
-
'code': auth_code,
|
127
|
-
'client_id': credentials['web']['client_id'],
|
128
|
-
'client_secret': credentials['web']['client_secret'],
|
129
|
-
'redirect_uri': REDIRECT_URI,
|
130
|
-
'grant_type': 'authorization_code'
|
131
|
-
}
|
132
|
-
response = requests.post(token_url, data=data)
|
133
|
-
log("Token exchange response: Status code {}, Body: {}".format(response.status_code, response.text))
|
134
|
-
token_response = response.json()
|
135
|
-
if response.status_code == 200:
|
136
|
-
token_response['token_time'] = time.time()
|
137
|
-
return token_response
|
138
|
-
else:
|
139
|
-
log("Token exchange failed: {}".format(token_response))
|
140
|
-
if attempt < retries - 1:
|
141
|
-
log("Retrying token exchange... (Attempt {}/{})".format(attempt + 1, retries))
|
142
|
-
except Exception as e:
|
143
|
-
log("Error during token exchange: {}".format(e))
|
144
|
-
return {}
|
114
|
+
return oauth_exchange_code_for_token(auth_code, CREDENTIALS_PATH, REDIRECT_URI, log, retries=retries)
|
145
115
|
|
146
116
|
def get_access_token():
|
147
117
|
if os.path.exists(TOKEN_PATH):
|
148
118
|
with open(TOKEN_PATH, 'r') as token_file:
|
149
119
|
token_data = json.load(token_file)
|
150
120
|
log("Loaded token data:\n {}".format(token_data))
|
151
|
-
|
152
121
|
if 'access_token' in token_data and 'expires_in' in token_data:
|
153
122
|
try:
|
154
|
-
# Use current time if 'token_time' is missing
|
155
123
|
token_time = token_data.get('token_time', time.time())
|
156
124
|
token_expiry_time = token_time + token_data['expires_in']
|
157
|
-
|
158
125
|
except KeyError as e:
|
159
126
|
log("KeyError while accessing token data: {}".format(e))
|
160
127
|
return None
|
161
|
-
|
162
128
|
if token_expiry_time > time.time():
|
163
129
|
log("Access token is still valid. Expires in {} seconds.".format(token_expiry_time - time.time()))
|
164
130
|
return token_data['access_token']
|
165
131
|
else:
|
166
132
|
log("Access token has expired. Current time: {}, Expiry time: {}".format(time.time(), token_expiry_time))
|
167
|
-
new_token_data =
|
133
|
+
new_token_data = oauth_refresh_access_token(token_data.get('refresh_token'), CREDENTIALS_PATH, log)
|
168
134
|
if 'access_token' in new_token_data:
|
169
135
|
new_token_data['token_time'] = time.time()
|
170
136
|
with open(TOKEN_PATH, 'w') as token_file:
|
@@ -178,44 +144,20 @@ def get_access_token():
|
|
178
144
|
return None
|
179
145
|
|
180
146
|
def refresh_access_token(refresh_token):
|
181
|
-
|
182
|
-
with open(CREDENTIALS_PATH, 'r') as credentials_file:
|
183
|
-
credentials = json.load(credentials_file)
|
184
|
-
token_url = "https://oauth2.googleapis.com/token"
|
185
|
-
data = {
|
186
|
-
'client_id': credentials['web']['client_id'],
|
187
|
-
'client_secret': credentials['web']['client_secret'],
|
188
|
-
'refresh_token': refresh_token,
|
189
|
-
'grant_type': 'refresh_token'
|
190
|
-
}
|
191
|
-
response = requests.post(token_url, data=data)
|
192
|
-
log("Refresh token response: Status code {}, Body:\n {}".format(response.status_code, response.text))
|
193
|
-
if response.status_code == 200:
|
194
|
-
log("Access token refreshed successfully.")
|
195
|
-
return response.json()
|
196
|
-
else:
|
197
|
-
log("Failed to refresh access token. Status code: {}".format(response.status_code))
|
198
|
-
return {}
|
147
|
+
return oauth_refresh_access_token(refresh_token, CREDENTIALS_PATH, log)
|
199
148
|
|
200
149
|
def bring_window_to_foreground():
|
201
150
|
"""Brings the current window to the foreground on Windows."""
|
202
151
|
try:
|
203
152
|
if platform.system() == 'Windows':
|
204
|
-
# Get the current process ID
|
205
153
|
pid = os.getpid()
|
206
|
-
# Get the window handle for the current process
|
207
154
|
hwnd = ctypes.windll.user32.GetForegroundWindow()
|
208
|
-
# Get the process ID of the window
|
209
155
|
current_pid = ctypes.c_ulong()
|
210
156
|
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(current_pid))
|
211
|
-
|
212
|
-
# If the window is not ours, try to bring it to front
|
213
157
|
if current_pid.value != pid:
|
214
|
-
# Try to set the window to foreground
|
215
158
|
ctypes.windll.user32.SetForegroundWindow(hwnd)
|
216
|
-
# If that fails, try the alternative method
|
217
159
|
if ctypes.windll.user32.GetForegroundWindow() != hwnd:
|
218
|
-
ctypes.windll.user32.ShowWindow(hwnd, 9)
|
160
|
+
ctypes.windll.user32.ShowWindow(hwnd, 9)
|
219
161
|
ctypes.windll.user32.SetForegroundWindow(hwnd)
|
220
162
|
except Exception as e:
|
221
163
|
log("Error bringing window to foreground: {}".format(e))
|
@@ -238,22 +180,17 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
238
180
|
post_data = self.rfile.read(content_length)
|
239
181
|
data = json.loads(post_data.decode('utf-8'))
|
240
182
|
links = data.get('links', [])
|
241
|
-
|
242
|
-
# Log the content of links
|
243
183
|
log("Received links: {}".format(links))
|
244
|
-
|
245
184
|
file_ids = [link.get('fileId', None) for link in links if link.get('fileId')]
|
246
185
|
log("File IDs received from client: {}".format(file_ids))
|
247
|
-
|
248
|
-
# Proceed with downloading files
|
249
186
|
download_docx_files(links)
|
250
187
|
self.send_response(200)
|
251
|
-
self._set_headers()
|
188
|
+
self._set_headers()
|
252
189
|
self.end_headers()
|
253
190
|
response = json.dumps({"status": "success", "message": "All files downloaded", "fileIds": file_ids})
|
254
191
|
self.wfile.write(response.encode('utf-8'))
|
255
192
|
shutdown_event.set()
|
256
|
-
bring_window_to_foreground()
|
193
|
+
bring_window_to_foreground()
|
257
194
|
elif self.path == '/shutdown':
|
258
195
|
log("Shutdown request received.")
|
259
196
|
self.send_response(200)
|
@@ -261,14 +198,13 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
261
198
|
self.end_headers()
|
262
199
|
response = json.dumps({"status": "success", "message": "Server is shutting down."})
|
263
200
|
self.wfile.write(response.encode('utf-8'))
|
264
|
-
shutdown_event.set()
|
201
|
+
shutdown_event.set()
|
265
202
|
elif self.path == '/delete-files':
|
266
203
|
content_length = int(self.headers['Content-Length'])
|
267
204
|
post_data = self.rfile.read(content_length)
|
268
205
|
data = json.loads(post_data.decode('utf-8'))
|
269
206
|
file_ids = data.get('fileIds', [])
|
270
207
|
log("File IDs to delete received from client: {}".format(file_ids))
|
271
|
-
|
272
208
|
if not isinstance(file_ids, list):
|
273
209
|
self.send_response(400)
|
274
210
|
self._set_headers()
|
@@ -276,9 +212,8 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
276
212
|
response = json.dumps({"status": "error", "message": "Invalid fileIds parameter."})
|
277
213
|
self.wfile.write(response.encode('utf-8'))
|
278
214
|
return
|
279
|
-
|
280
215
|
self.send_response(200)
|
281
|
-
self._set_headers()
|
216
|
+
self._set_headers()
|
282
217
|
self.end_headers()
|
283
218
|
response = json.dumps({"status": "success", "message": "Files deleted successfully."})
|
284
219
|
self.wfile.write(response.encode('utf-8'))
|
@@ -287,23 +222,21 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
287
222
|
self.end_headers()
|
288
223
|
|
289
224
|
def do_GET(self):
|
290
|
-
log("Full request path: {}".format(self.path))
|
225
|
+
log("Full request path: {}".format(self.path))
|
291
226
|
if self.path.startswith("/?code="):
|
292
227
|
auth_code = self.path.split('=')[1].split('&')[0]
|
293
|
-
auth_code = requests.utils.unquote(auth_code)
|
228
|
+
auth_code = requests.utils.unquote(auth_code)
|
294
229
|
log("Received authorization code: {}".format(auth_code))
|
295
|
-
if
|
230
|
+
if oauth_is_valid_authorization_code(auth_code, log):
|
296
231
|
try:
|
297
232
|
token_response = exchange_code_for_token(auth_code)
|
298
233
|
if 'access_token' not in token_response:
|
299
|
-
# Check for specific error message
|
300
234
|
if token_response.get("status") == "error":
|
301
235
|
self.send_response(400)
|
302
236
|
self.send_header('Content-type', 'text/html')
|
303
237
|
self.end_headers()
|
304
238
|
self.wfile.write(token_response["message"].encode())
|
305
239
|
return
|
306
|
-
# Handle other cases
|
307
240
|
raise ValueError("Access token not found in response.")
|
308
241
|
except Exception as e:
|
309
242
|
log("Error during token exchange: {}".format(e))
|
@@ -312,7 +245,7 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
312
245
|
self.end_headers()
|
313
246
|
self.wfile.write("An error occurred during authentication. Please try again.".encode())
|
314
247
|
else:
|
315
|
-
log("Token response: {}".format(token_response))
|
248
|
+
log("Token response: {}".format(token_response))
|
316
249
|
if 'access_token' in token_response:
|
317
250
|
with open(TOKEN_PATH, 'w') as token_file:
|
318
251
|
json.dump(token_response, token_file)
|
@@ -320,34 +253,31 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
320
253
|
self.send_header('Content-type', 'text/html')
|
321
254
|
self.end_headers()
|
322
255
|
self.wfile.write("Authentication successful. You can close this window now.".encode())
|
323
|
-
initiate_link_retrieval(config)
|
256
|
+
initiate_link_retrieval(config)
|
324
257
|
else:
|
325
|
-
log("Authentication failed with response: {}".format(token_response))
|
258
|
+
log("Authentication failed with response: {}".format(token_response))
|
326
259
|
if 'error' in token_response:
|
327
260
|
error_description = token_response.get('error_description', 'No description provided.')
|
328
|
-
log("Error details: {}".format(error_description))
|
329
|
-
|
330
|
-
# Provide user feedback based on the error
|
261
|
+
log("Error details: {}".format(error_description))
|
331
262
|
if token_response.get('error') == 'invalid_grant':
|
332
263
|
log("Invalid grant error encountered. Authorization code: {}, Response: {}".format(auth_code, token_response))
|
333
264
|
check_invalid_grant_causes(auth_code)
|
334
|
-
|
265
|
+
oauth_clear_token_cache(TOKEN_PATH, log)
|
335
266
|
user_message = "Authentication failed: Invalid or expired authorization code. Please try again."
|
336
267
|
else:
|
337
268
|
user_message = "Authentication failed. Please check the logs for more details."
|
338
|
-
|
339
269
|
self.send_response(400)
|
340
270
|
self.send_header('Content-type', 'text/html')
|
341
271
|
self.end_headers()
|
342
272
|
self.wfile.write(user_message.encode())
|
343
|
-
shutdown_event.set()
|
273
|
+
shutdown_event.set()
|
344
274
|
else:
|
345
275
|
log("Invalid authorization code format: {}".format(auth_code))
|
346
276
|
self.send_response(400)
|
347
277
|
self.send_header('Content-type', 'text/html')
|
348
278
|
self.end_headers()
|
349
279
|
self.wfile.write("Invalid authorization code format. Please try again.".encode())
|
350
|
-
shutdown_event.set()
|
280
|
+
shutdown_event.set()
|
351
281
|
elif self.path == '/downloaded-emails':
|
352
282
|
self.send_response(200)
|
353
283
|
self._set_headers()
|
@@ -365,75 +295,17 @@ class RequestHandler(BaseHTTPRequestHandler):
|
|
365
295
|
self.wfile.write(b'HTTPS server is running.')
|
366
296
|
|
367
297
|
def generate_self_signed_cert(cert_file, key_file):
|
368
|
-
|
369
|
-
log("Checking if key file exists: " + key_file)
|
370
|
-
|
371
|
-
# Check if certificate exists and is not expired
|
372
|
-
cert_needs_regeneration = True
|
373
|
-
if os.path.exists(cert_file):
|
374
|
-
try:
|
375
|
-
# Check certificate expiration
|
376
|
-
check_cmd = ['openssl', 'x509', '-in', cert_file, '-checkend', '86400', '-noout'] # Check if expires in next 24 hours
|
377
|
-
result = subprocess.call(check_cmd)
|
378
|
-
if result == 0:
|
379
|
-
log("Certificate is still valid")
|
380
|
-
cert_needs_regeneration = False
|
381
|
-
else:
|
382
|
-
log("Certificate is expired or will expire soon")
|
383
|
-
# Delete expired certificate and key files
|
384
|
-
try:
|
385
|
-
if os.path.exists(cert_file):
|
386
|
-
os.remove(cert_file)
|
387
|
-
log("Deleted expired certificate file: {}".format(cert_file))
|
388
|
-
if os.path.exists(key_file):
|
389
|
-
os.remove(key_file)
|
390
|
-
log("Deleted expired key file: {}".format(key_file))
|
391
|
-
except Exception as e:
|
392
|
-
log("Error deleting expired certificate files: {}".format(e))
|
393
|
-
except Exception as e:
|
394
|
-
log("Error checking certificate expiration: {}".format(e))
|
395
|
-
|
396
|
-
if cert_needs_regeneration:
|
397
|
-
log("Generating self-signed SSL certificate...")
|
398
|
-
cmd = [
|
399
|
-
'openssl', 'req', '-config', openssl_cnf, '-nodes', '-new', '-x509',
|
400
|
-
'-keyout', key_file,
|
401
|
-
'-out', cert_file,
|
402
|
-
'-days', '365',
|
403
|
-
'-sha256' # Use SHA-256 for better security
|
404
|
-
#'-subj', '/C=US/ST=...' The openssl.cnf file contains default values for these fields, but they can be overridden by the -subj option.
|
405
|
-
]
|
406
|
-
try:
|
407
|
-
log("Running command: " + ' '.join(cmd))
|
408
|
-
result = subprocess.call(cmd)
|
409
|
-
log("Command finished with result: " + str(result))
|
410
|
-
if result != 0:
|
411
|
-
raise RuntimeError("Failed to generate self-signed certificate")
|
412
|
-
|
413
|
-
# Verify the certificate was generated correctly
|
414
|
-
verify_cmd = ['openssl', 'x509', '-in', cert_file, '-text', '-noout']
|
415
|
-
verify_result = subprocess.call(verify_cmd)
|
416
|
-
if verify_result != 0:
|
417
|
-
raise RuntimeError("Generated certificate verification failed")
|
418
|
-
|
419
|
-
log("Self-signed SSL certificate generated and verified successfully.")
|
420
|
-
except Exception as e:
|
421
|
-
log("Error generating self-signed certificate: {}".format(e))
|
422
|
-
raise
|
298
|
+
http_generate_self_signed_cert(openssl_cnf, cert_file, key_file, log, subprocess)
|
423
299
|
|
424
300
|
def run_server():
|
425
301
|
global httpd
|
426
302
|
try:
|
427
303
|
log("Attempting to start server on port " + str(server_port))
|
428
|
-
server_address = ('0.0.0.0', server_port) # Bind to all interfaces
|
429
|
-
httpd = HTTPServer(server_address, RequestHandler)
|
430
|
-
log("Attempting to wrap socket with SSL. cert_file=" + cert_file + ", key_file=" + key_file)
|
431
|
-
|
432
304
|
if not os.path.exists(cert_file):
|
433
305
|
log("Error: Certificate file not found: " + cert_file)
|
434
306
|
if not os.path.exists(key_file):
|
435
307
|
log("Error: Key file not found: " + key_file)
|
436
|
-
|
308
|
+
httpd = HTTPServer(('0.0.0.0', server_port), RequestHandler)
|
437
309
|
httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, keyfile=key_file, server_side=True)
|
438
310
|
log("Starting HTTPS server on port {}".format(server_port))
|
439
311
|
httpd.serve_forever()
|
@@ -448,8 +320,8 @@ def stop_server():
|
|
448
320
|
httpd.shutdown()
|
449
321
|
httpd.server_close()
|
450
322
|
log("HTTPS server stopped.")
|
451
|
-
shutdown_event.set()
|
452
|
-
bring_window_to_foreground()
|
323
|
+
shutdown_event.set()
|
324
|
+
bring_window_to_foreground()
|
453
325
|
|
454
326
|
def load_downloaded_emails():
|
455
327
|
downloaded_emails = set()
|
@@ -460,86 +332,26 @@ def load_downloaded_emails():
|
|
460
332
|
return downloaded_emails
|
461
333
|
|
462
334
|
def download_docx_files(links):
|
463
|
-
# Load the set of downloaded emails
|
464
|
-
# TODO (LOW-MEDIUM PRIORITY - CSV File Detection and Routing):
|
465
|
-
# PROBLEM: Downloaded files may include CSV files that need special handling and routing.
|
466
|
-
# Currently all files are treated the same regardless of extension.
|
467
|
-
#
|
468
|
-
# IMPLEMENTATION REQUIREMENTS:
|
469
|
-
# 1. File Extension Detection:
|
470
|
-
# - Check each downloaded file for .csv extension (case-insensitive)
|
471
|
-
# - Also check for common CSV variants: .txt, .tsv, .dat (based on content)
|
472
|
-
# - Handle files with multiple extensions like "report.csv.zip"
|
473
|
-
#
|
474
|
-
# 2. Content-Based Detection (Advanced):
|
475
|
-
# - For files without clear extensions, peek at content
|
476
|
-
# - Look for CSV patterns: comma-separated values, consistent column counts
|
477
|
-
# - Handle Excel files that might be CSV exports (.xlsx with CSV content)
|
478
|
-
#
|
479
|
-
# 3. CSV Routing Logic:
|
480
|
-
# - Move CSV files to dedicated CSV processing directory
|
481
|
-
# - Maintain file naming conventions for downstream processing
|
482
|
-
# - Log CSV file movements for audit trail
|
483
|
-
# - Preserve original file permissions and timestamps
|
484
|
-
#
|
485
|
-
# IMPLEMENTATION STEPS:
|
486
|
-
# 1. Add helper function detect_csv_files(downloaded_files) -> list
|
487
|
-
# 2. Add helper function move_csv_to_processing_dir(csv_file, destination_dir)
|
488
|
-
# 3. Add configuration for CSV destination directory in config file
|
489
|
-
# 4. Update this function to call CSV detection and routing after download
|
490
|
-
# 5. Add error handling for file movement failures
|
491
|
-
# 6. Add logging for all CSV file operations
|
492
|
-
#
|
493
|
-
# CONFIGURATION NEEDED:
|
494
|
-
# - config['csv_processing_dir']: Where to move detected CSV files
|
495
|
-
# - config['csv_file_extensions']: List of extensions to treat as CSV
|
496
|
-
# - config['csv_content_detection']: Boolean to enable content-based detection
|
497
|
-
#
|
498
|
-
# ERROR HANDLING:
|
499
|
-
# - Handle permission errors when moving files
|
500
|
-
# - Handle disk space issues
|
501
|
-
# - Gracefully handle corrupted or locked files
|
502
|
-
# - Provide fallback options when CSV directory is unavailable
|
503
|
-
#
|
504
|
-
# TESTING SCENARIOS:
|
505
|
-
# - Mixed file types: .docx, .csv, .pdf in same download batch
|
506
|
-
# - CSV files with unusual extensions (.txt, .dat)
|
507
|
-
# - Large CSV files (>100MB)
|
508
|
-
# - CSV files in ZIP archives
|
509
|
-
#
|
510
|
-
# FILES TO MODIFY: This file (download_docx_files function)
|
511
|
-
# RELATED: May need updates to CSV processing modules that expect files in specific locations
|
512
335
|
downloaded_emails = load_downloaded_emails()
|
513
|
-
|
514
336
|
for link in links:
|
515
337
|
try:
|
516
338
|
url = link.get('url', '')
|
517
339
|
filename = link.get('filename', '')
|
518
|
-
|
519
|
-
# Log the variables to debug
|
520
340
|
log("Processing link: url='{}', filename='{}'".format(url, filename))
|
521
|
-
|
522
|
-
# CSV ROUTING PLACEHOLDER:
|
523
|
-
# - Detect CSV-like extensions before download and log the intended routing decision.
|
524
|
-
# - This is a no-op for now to avoid side-effects until the pipeline is verified on XP.
|
525
341
|
lower_name = (filename or '').lower()
|
526
342
|
looks_like_csv = any(lower_name.endswith(ext) for ext in ['.csv', '.tsv', '.txt', '.dat'])
|
527
343
|
if looks_like_csv:
|
528
344
|
log("[CSV Routing Preview] Detected CSV-like filename: {}. Would route to CSV processing directory.".format(filename))
|
529
|
-
|
530
|
-
# Skip if email already downloaded
|
531
345
|
if filename in downloaded_emails:
|
532
346
|
log("Skipping already downloaded email: {}".format(filename))
|
533
347
|
continue
|
534
|
-
|
535
348
|
log("Downloading .docx file from URL: {}".format(url))
|
536
|
-
response = requests.get(url, verify=False)
|
349
|
+
response = requests.get(url, verify=False)
|
537
350
|
if response.status_code == 200:
|
538
351
|
file_path = os.path.join(local_storage_path, filename)
|
539
352
|
with open(file_path, 'wb') as file:
|
540
353
|
file.write(response.content)
|
541
354
|
log("Downloaded .docx file: {}".format(filename))
|
542
|
-
# Add to the set and save the updated list
|
543
355
|
downloaded_emails.add(filename)
|
544
356
|
with open(downloaded_emails_file, 'a') as file:
|
545
357
|
file.write(filename + '\n')
|
@@ -567,59 +379,44 @@ def open_browser_with_executable(url, browser_path=None):
|
|
567
379
|
|
568
380
|
def initiate_link_retrieval(config):
|
569
381
|
log("Initiating browser via implicit GET.")
|
570
|
-
url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id'])
|
382
|
+
url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(config['MediLink_Config']['webapp_deployment_id'])
|
571
383
|
open_browser_with_executable(url_get)
|
572
|
-
|
573
384
|
log("Preparing POST call.")
|
574
|
-
url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id'])
|
385
|
+
url = "https://script.google.com/macros/s/{}/exec".format(config['MediLink_Config']['webapp_deployment_id'])
|
575
386
|
downloaded_emails = list(load_downloaded_emails())
|
576
|
-
payload = {
|
577
|
-
"downloadedEmails": downloaded_emails
|
578
|
-
}
|
579
|
-
|
387
|
+
payload = {"downloadedEmails": downloaded_emails}
|
580
388
|
access_token = get_access_token()
|
581
389
|
if not access_token:
|
582
390
|
log("Access token not found. Please authenticate first.")
|
583
|
-
shutdown_event.set()
|
391
|
+
shutdown_event.set()
|
584
392
|
return
|
585
|
-
|
586
|
-
# Inspect the token to check its validity and permissions
|
587
|
-
token_info = inspect_token(access_token)
|
393
|
+
token_info = http_inspect_token(access_token, log, delete_token_file_fn=delete_token_file, stop_server_fn=stop_server)
|
588
394
|
if token_info is None:
|
589
395
|
log("Access token is invalid. Please re-authenticate.")
|
590
|
-
shutdown_event.set()
|
396
|
+
shutdown_event.set()
|
591
397
|
return
|
592
|
-
|
593
|
-
# Proceed with the rest of the function if the token is valid
|
594
|
-
headers = {
|
595
|
-
'Authorization': 'Bearer {}'.format(access_token),
|
596
|
-
'Content-Type': 'application/json'
|
597
|
-
}
|
598
|
-
|
398
|
+
headers = {'Authorization': 'Bearer {}'.format(access_token), 'Content-Type': 'application/json'}
|
599
399
|
log("Request headers: {}".format(headers))
|
600
400
|
log("Request payload: {}".format(payload))
|
601
|
-
|
602
401
|
handle_post_response(url, payload, headers)
|
603
|
-
|
402
|
+
|
604
403
|
def handle_post_response(url, payload, headers):
|
605
404
|
try:
|
606
405
|
response = requests.post(url, json=payload, headers=headers)
|
607
406
|
log("Response status code: {}".format(response.status_code))
|
608
407
|
log("Response body: {}".format(response.text))
|
609
|
-
|
610
408
|
if response.status_code == 200:
|
611
409
|
response_data = response.json()
|
612
|
-
log("Parsed response data: {}".format(response_data))
|
410
|
+
log("Parsed response data: {}".format(response_data))
|
613
411
|
if response_data.get("status") == "error":
|
614
412
|
log("Error message from server: {}".format(response_data.get("message")))
|
615
413
|
print("Error: {}".format(response_data.get("message")))
|
616
|
-
shutdown_event.set()
|
414
|
+
shutdown_event.set()
|
617
415
|
else:
|
618
416
|
log("Link retrieval initiated successfully.")
|
619
417
|
elif response.status_code == 401:
|
620
418
|
log("Unauthorized. Check if the token has the necessary scopes.Response body: {}".format(response.text))
|
621
|
-
|
622
|
-
token_info = inspect_token(headers['Authorization'].split(' ')[1])
|
419
|
+
token_info = http_inspect_token(headers['Authorization'].split(' ')[1], log, delete_token_file_fn=delete_token_file, stop_server_fn=stop_server)
|
623
420
|
log("Token details: {}".format(token_info))
|
624
421
|
shutdown_event.set()
|
625
422
|
elif response.status_code == 403:
|
@@ -639,26 +436,7 @@ def handle_post_response(url, payload, headers):
|
|
639
436
|
shutdown_event.set()
|
640
437
|
|
641
438
|
def inspect_token(access_token):
|
642
|
-
|
643
|
-
try:
|
644
|
-
response = requests.get(info_url)
|
645
|
-
log("Token info: Status code {}, Body: {}".format(response.status_code, response.text))
|
646
|
-
|
647
|
-
if response.status_code == 200:
|
648
|
-
return response.json()
|
649
|
-
else:
|
650
|
-
log("Failed to inspect token. Status code: {}, Body: {}".format(response.status_code, response.text))
|
651
|
-
# Check for invalid token
|
652
|
-
if response.status_code == 400 and "invalid_token" in response.text:
|
653
|
-
log("Access token is invalid. Deleting token.json and stopping the server.")
|
654
|
-
delete_token_file() # Delete the token.json file
|
655
|
-
print("Access token is invalid. Please re-authenticate and restart the server.")
|
656
|
-
stop_server() # Stop the server
|
657
|
-
return None # Return None for invalid tokens
|
658
|
-
return None # Return None for other invalid tokens
|
659
|
-
except Exception as e:
|
660
|
-
log("Exception during token inspection: {}".format(e))
|
661
|
-
return None
|
439
|
+
return http_inspect_token(access_token, log, delete_token_file_fn=delete_token_file, stop_server_fn=stop_server)
|
662
440
|
|
663
441
|
def delete_token_file():
|
664
442
|
try:
|
@@ -679,67 +457,38 @@ def auth_and_retrieval():
|
|
679
457
|
access_token = get_access_token()
|
680
458
|
if not access_token:
|
681
459
|
log("Access token not found or expired. Please authenticate first.")
|
682
|
-
#print("If the browser does not open automatically, please open the following URL in your browser to authorize the application:")
|
683
460
|
auth_url = get_authorization_url()
|
684
|
-
#print(auth_url)
|
685
461
|
open_browser_with_executable(auth_url)
|
686
|
-
shutdown_event.wait()
|
462
|
+
shutdown_event.wait()
|
687
463
|
else:
|
688
464
|
log("Access token found. Proceeding.")
|
689
|
-
initiate_link_retrieval(config)
|
690
|
-
shutdown_event.wait()
|
465
|
+
initiate_link_retrieval(config)
|
466
|
+
shutdown_event.wait()
|
691
467
|
|
692
468
|
def is_valid_authorization_code(auth_code):
|
693
|
-
|
694
|
-
if auth_code and isinstance(auth_code, str) and len(auth_code) > 0: # Check for non-empty string
|
695
|
-
return True
|
696
|
-
log("Invalid authorization code format: {}".format(auth_code))
|
697
|
-
return False
|
469
|
+
return oauth_is_valid_authorization_code(auth_code, log)
|
698
470
|
|
699
471
|
def clear_token_cache():
|
700
|
-
|
701
|
-
os.remove(TOKEN_PATH)
|
702
|
-
log("Cleared token cache.")
|
472
|
+
oauth_clear_token_cache(TOKEN_PATH, log)
|
703
473
|
|
704
474
|
def check_invalid_grant_causes(auth_code):
|
705
|
-
# TODO Implement this function in the future to check for common causes of invalid_grant error
|
706
|
-
# Log potential causes for invalid_grant
|
707
|
-
# XP/Network NOTE: On older systems, clock skew and reused codes are frequent causes.
|
708
475
|
log("FUTURE IMPLEMENTATION: Checking common causes for invalid_grant error with auth code: {}".format(auth_code))
|
709
|
-
# Suggested checks (to be implemented when plumbing is ready):
|
710
|
-
# - Has authorization code already been used?
|
711
|
-
# - Does redirect URI exactly match the one registered (case and trailing slashes)?
|
712
|
-
# - Is system clock skewed? Compare to Google time; log skew if detected.
|
713
|
-
# - Are the requested scopes enabled for this OAuth client?
|
714
|
-
# - Did the user revoke access between code issuance and token exchange?
|
715
|
-
# Each of these would produce a specific log to speed up troubleshooting on XP.
|
716
|
-
"""
|
717
|
-
if is_code_used(auth_code):
|
718
|
-
log("Authorization code has already been used.")
|
719
|
-
if not is_redirect_uri_correct():
|
720
|
-
log("Redirect URI does not match the registered URI.")
|
721
|
-
"""
|
722
476
|
|
723
477
|
if __name__ == "__main__":
|
724
478
|
signal.signal(signal.SIGINT, signal_handler)
|
725
479
|
signal.signal(signal.SIGTERM, signal_handler)
|
726
|
-
|
727
480
|
try:
|
728
|
-
# Generate SSL certificate if it doesn't exist
|
729
481
|
generate_self_signed_cert(cert_file, key_file)
|
730
|
-
|
731
482
|
from threading import Thread
|
732
483
|
log("Starting server thread.")
|
733
484
|
server_thread = Thread(target=run_server)
|
734
485
|
server_thread.daemon = True
|
735
486
|
server_thread.start()
|
736
|
-
|
737
487
|
auth_and_retrieval()
|
738
|
-
|
739
488
|
log("Stopping HTTPS server.")
|
740
|
-
stop_server()
|
489
|
+
stop_server()
|
741
490
|
log("Waiting for server thread to finish.")
|
742
|
-
server_thread.join()
|
491
|
+
server_thread.join()
|
743
492
|
except KeyboardInterrupt:
|
744
493
|
log("KeyboardInterrupt received, stopping server.")
|
745
494
|
stop_server()
|
@@ -0,0 +1,88 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import ssl
|
4
|
+
import requests
|
5
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
6
|
+
|
7
|
+
|
8
|
+
def generate_self_signed_cert(openssl_cnf_path, cert_file, key_file, log, subprocess_module):
|
9
|
+
log("Checking if certificate file exists: " + cert_file)
|
10
|
+
log("Checking if key file exists: " + key_file)
|
11
|
+
|
12
|
+
cert_needs_regeneration = True
|
13
|
+
if os.path.exists(cert_file):
|
14
|
+
try:
|
15
|
+
check_cmd = ['openssl', 'x509', '-in', cert_file, '-checkend', '86400', '-noout']
|
16
|
+
result = subprocess_module.call(check_cmd)
|
17
|
+
if result == 0:
|
18
|
+
log("Certificate is still valid")
|
19
|
+
cert_needs_regeneration = False
|
20
|
+
else:
|
21
|
+
log("Certificate is expired or will expire soon")
|
22
|
+
try:
|
23
|
+
if os.path.exists(cert_file):
|
24
|
+
os.remove(cert_file)
|
25
|
+
log("Deleted expired certificate file: {}".format(cert_file))
|
26
|
+
if os.path.exists(key_file):
|
27
|
+
os.remove(key_file)
|
28
|
+
log("Deleted expired key file: {}".format(key_file))
|
29
|
+
except Exception as e:
|
30
|
+
log("Error deleting expired certificate files: {}".format(e))
|
31
|
+
except Exception as e:
|
32
|
+
log("Error checking certificate expiration: {}".format(e))
|
33
|
+
|
34
|
+
if cert_needs_regeneration:
|
35
|
+
log("Generating self-signed SSL certificate...")
|
36
|
+
cmd = [
|
37
|
+
'openssl', 'req', '-config', openssl_cnf_path, '-nodes', '-new', '-x509',
|
38
|
+
'-keyout', key_file,
|
39
|
+
'-out', cert_file,
|
40
|
+
'-days', '365',
|
41
|
+
'-sha256'
|
42
|
+
]
|
43
|
+
try:
|
44
|
+
log("Running command: " + ' '.join(cmd))
|
45
|
+
result = subprocess_module.call(cmd)
|
46
|
+
log("Command finished with result: " + str(result))
|
47
|
+
if result != 0:
|
48
|
+
raise RuntimeError("Failed to generate self-signed certificate")
|
49
|
+
verify_cmd = ['openssl', 'x509', '-in', cert_file, '-text', '-noout']
|
50
|
+
verify_result = subprocess_module.call(verify_cmd)
|
51
|
+
if verify_result != 0:
|
52
|
+
raise RuntimeError("Generated certificate verification failed")
|
53
|
+
log("Self-signed SSL certificate generated and verified successfully.")
|
54
|
+
except Exception as e:
|
55
|
+
log("Error generating self-signed certificate: {}".format(e))
|
56
|
+
raise
|
57
|
+
|
58
|
+
|
59
|
+
def start_https_server(port, handler_cls, cert_file, key_file, log):
|
60
|
+
server_address = ('0.0.0.0', port)
|
61
|
+
httpd = HTTPServer(server_address, handler_cls)
|
62
|
+
log("Attempting to wrap socket with SSL. cert_file=" + cert_file + ", key_file=" + key_file)
|
63
|
+
httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert_file, keyfile=key_file, server_side=True)
|
64
|
+
log("Starting HTTPS server on port {}".format(port))
|
65
|
+
httpd.serve_forever()
|
66
|
+
return httpd
|
67
|
+
|
68
|
+
|
69
|
+
def inspect_token(access_token, log, delete_token_file_fn=None, stop_server_fn=None):
|
70
|
+
info_url = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={}".format(access_token)
|
71
|
+
try:
|
72
|
+
response = requests.get(info_url)
|
73
|
+
log("Token info: Status code {}, Body: {}".format(response.status_code, response.text))
|
74
|
+
if response.status_code == 200:
|
75
|
+
return response.json()
|
76
|
+
else:
|
77
|
+
log("Failed to inspect token. Status code: {}, Body: {}".format(response.status_code, response.text))
|
78
|
+
if response.status_code == 400 and "invalid_token" in response.text:
|
79
|
+
log("Access token is invalid. Deleting token.json and stopping the server.")
|
80
|
+
if delete_token_file_fn:
|
81
|
+
delete_token_file_fn()
|
82
|
+
if stop_server_fn:
|
83
|
+
stop_server_fn()
|
84
|
+
return None
|
85
|
+
return None
|
86
|
+
except Exception as e:
|
87
|
+
log("Exception during token inspection: {}".format(e))
|
88
|
+
return None
|
@@ -0,0 +1,98 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import time
|
4
|
+
import requests
|
5
|
+
|
6
|
+
|
7
|
+
def get_authorization_url(credentials_path, redirect_uri, scopes, log):
|
8
|
+
"""
|
9
|
+
Build the Google OAuth authorization URL using the provided credentials file, redirect URI, and scopes.
|
10
|
+
"""
|
11
|
+
with open(credentials_path, 'r') as credentials_file:
|
12
|
+
credentials = json.load(credentials_file)
|
13
|
+
client_id = credentials['web']['client_id']
|
14
|
+
auth_url = (
|
15
|
+
"https://accounts.google.com/o/oauth2/v2/auth?"
|
16
|
+
"response_type=code&"
|
17
|
+
"client_id={}&"
|
18
|
+
"redirect_uri={}&"
|
19
|
+
"scope={}&"
|
20
|
+
"access_type=offline&"
|
21
|
+
"prompt=consent"
|
22
|
+
).format(client_id, redirect_uri, scopes)
|
23
|
+
log("Generated authorization URL: {}".format(auth_url))
|
24
|
+
return auth_url
|
25
|
+
|
26
|
+
|
27
|
+
def exchange_code_for_token(auth_code, credentials_path, redirect_uri, log, retries=3):
|
28
|
+
"""
|
29
|
+
Exchange an authorization code for tokens using credentials; retries a few times on failure.
|
30
|
+
"""
|
31
|
+
for attempt in range(retries):
|
32
|
+
try:
|
33
|
+
with open(credentials_path, 'r') as credentials_file:
|
34
|
+
credentials = json.load(credentials_file)
|
35
|
+
token_url = "https://oauth2.googleapis.com/token"
|
36
|
+
data = {
|
37
|
+
'code': auth_code,
|
38
|
+
'client_id': credentials['web']['client_id'],
|
39
|
+
'client_secret': credentials['web']['client_secret'],
|
40
|
+
'redirect_uri': redirect_uri,
|
41
|
+
'grant_type': 'authorization_code'
|
42
|
+
}
|
43
|
+
response = requests.post(token_url, data=data)
|
44
|
+
log("Token exchange response: Status code {}, Body: {}".format(response.status_code, response.text))
|
45
|
+
token_response = response.json()
|
46
|
+
if response.status_code == 200:
|
47
|
+
token_response['token_time'] = time.time()
|
48
|
+
return token_response
|
49
|
+
else:
|
50
|
+
log("Token exchange failed: {}".format(token_response))
|
51
|
+
if attempt < retries - 1:
|
52
|
+
log("Retrying token exchange... (Attempt {}/{})".format(attempt + 1, retries))
|
53
|
+
except Exception as e:
|
54
|
+
log("Error during token exchange: {}".format(e))
|
55
|
+
return {}
|
56
|
+
|
57
|
+
|
58
|
+
def refresh_access_token(refresh_token, credentials_path, log):
|
59
|
+
"""
|
60
|
+
Refresh an access token using the stored client credentials.
|
61
|
+
"""
|
62
|
+
log("Refreshing access token.")
|
63
|
+
with open(credentials_path, 'r') as credentials_file:
|
64
|
+
credentials = json.load(credentials_file)
|
65
|
+
token_url = "https://oauth2.googleapis.com/token"
|
66
|
+
data = {
|
67
|
+
'client_id': credentials['web']['client_id'],
|
68
|
+
'client_secret': credentials['web']['client_secret'],
|
69
|
+
'refresh_token': refresh_token,
|
70
|
+
'grant_type': 'refresh_token'
|
71
|
+
}
|
72
|
+
response = requests.post(token_url, data=data)
|
73
|
+
log("Refresh token response: Status code {}, Body:\n {}".format(response.status_code, response.text))
|
74
|
+
if response.status_code == 200:
|
75
|
+
log("Access token refreshed successfully.")
|
76
|
+
return response.json()
|
77
|
+
else:
|
78
|
+
log("Failed to refresh access token. Status code: {}".format(response.status_code))
|
79
|
+
return {}
|
80
|
+
|
81
|
+
|
82
|
+
def is_valid_authorization_code(auth_code, log):
|
83
|
+
"""
|
84
|
+
Validate auth code shape without side effects.
|
85
|
+
"""
|
86
|
+
if auth_code and isinstance(auth_code, str) and len(auth_code) > 0:
|
87
|
+
return True
|
88
|
+
log("Invalid authorization code format: {}".format(auth_code))
|
89
|
+
return False
|
90
|
+
|
91
|
+
|
92
|
+
def clear_token_cache(token_path, log):
|
93
|
+
"""
|
94
|
+
Delete token cache file if present.
|
95
|
+
"""
|
96
|
+
if os.path.exists(token_path):
|
97
|
+
os.remove(token_path)
|
98
|
+
log("Cleared token cache.")
|
@@ -6,7 +6,7 @@ MediBot/MediBot_Crosswalk_Utils.py,sha256=HQXZUWDZF_LvOMLsdjlsDQ79Yi_IiqNVRKX2Cd
|
|
6
6
|
MediBot/MediBot_Post.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
7
|
MediBot/MediBot_Preprocessor.py,sha256=zAcfyuE8wl9JRzLGsUnnXiHxAr-hbCCIB2M-Jb3LUqI,16203
|
8
8
|
MediBot/MediBot_Preprocessor_lib.py,sha256=L5VkBoSWuvvKNs0YoHz_-KQ5Z2hnqLJyM_j7yeKb9CM,72423
|
9
|
-
MediBot/MediBot_UI.py,sha256=
|
9
|
+
MediBot/MediBot_UI.py,sha256=YgNr_E8ahf7Y-Emkq04PavJH37LShs83wVxp9i6EVgw,14191
|
10
10
|
MediBot/MediBot_dataformat_library.py,sha256=XCkwhpkDbhiSDQ_21xPafqJzZLoq-C0dczoN2Xl5tHE,10260
|
11
11
|
MediBot/MediBot_docx_decoder.py,sha256=gn7I7Ng5khVIzU0HTTOqi31YSSn1yW8Pyk-i_P9r1oA,32472
|
12
12
|
MediBot/MediBot_smart_import.py,sha256=_ggP0I9djNwMGck04U6fNahrH7frVkoM8OzjtHbLUc0,6727
|
@@ -16,14 +16,14 @@ MediBot/__init__.py,sha256=6IdVLXaWxV5ZdpefonWrC1R8RsJn4V26K0PmUEZ_vU8,3192
|
|
16
16
|
MediBot/get_medicafe_version.py,sha256=uyL_UIE42MyFuJ3SRYxJp8sZx8xjTqlYZ3FdQuxLduY,728
|
17
17
|
MediBot/update_json.py,sha256=vvUF4mKCuaVly8MmoadDO59M231fCIInc0KI1EtDtPA,3704
|
18
18
|
MediBot/update_medicafe.py,sha256=i2-6b2y9XXaroPG8uP_RPFwOOsQSY_dTiuhl6vG8uoo,25693
|
19
|
-
MediCafe/MediLink_ConfigLoader.py,sha256=
|
19
|
+
MediCafe/MediLink_ConfigLoader.py,sha256=Ia79dZQBvgbc6CtOaNZVlFHaN-fvUmJRpmmVHz_MFv8,8205
|
20
20
|
MediCafe/__init__.py,sha256=DF0XUu3G43AejXvEmd5aCyy0GDQahQD0pMwexmxem-E,5477
|
21
21
|
MediCafe/__main__.py,sha256=mRNyk3D9Ilnu2XhgVI_rut7r5Ro7UIKtwV871giAHI8,12992
|
22
22
|
MediCafe/api_core.py,sha256=AwnB2BMiBOMS_LKJlkHgd_lQPpE57WEYMfS-O-DeOzo,63851
|
23
23
|
MediCafe/api_core_backup.py,sha256=Oy_Fqt0SEvGkQN1Oqw5iUPVFxPEokyju5CuPEb9k0OY,18686
|
24
24
|
MediCafe/api_factory.py,sha256=I5AeJoyu6m7oCrjc2OvVvO_4KSBRutTsR1riiWhTZV0,12086
|
25
25
|
MediCafe/api_utils.py,sha256=KWQB0q1k5E6frOFFlKWcFpHNcqfrS7KJ_82672wbupw,14041
|
26
|
-
MediCafe/core_utils.py,sha256=
|
26
|
+
MediCafe/core_utils.py,sha256=yftnn2pMhfUMEHfGl9ocvLoI8kQvsDjqrRZAR3uHdWo,26317
|
27
27
|
MediCafe/graphql_utils.py,sha256=5i_pNCNRUXox2v5zraPzUw4n7rUt9ZEAkLqVa59LWAc,45713
|
28
28
|
MediCafe/logging_config.py,sha256=auT65LN5oDEXVhkMeLke63kJHTWxYf2o8YihAfQFgzU,5493
|
29
29
|
MediCafe/logging_demo.py,sha256=TwUhzafna5pMdN3zSKGrpUWRqX96F1JGGsSUtr3dygs,1975
|
@@ -49,7 +49,7 @@ MediLink/MediLink_Deductible_Validator.py,sha256=2g-lZd-Y5fJ1mfP87vM6oABg0t5Om-7
|
|
49
49
|
MediLink/MediLink_Display_Utils.py,sha256=Bl15Ofqh09KIYsNXzM6WIE97Gp_4RVUw43J0NUzIERY,3121
|
50
50
|
MediLink/MediLink_Down.py,sha256=vZEFNWa6drpXK8DCzt3DAlHdPGdhv3HoLnQh9cppT8o,11793
|
51
51
|
MediLink/MediLink_ERA_decoder.py,sha256=MiOtDcXnmevPfHAahIlTLlUc14VcQWAor9Xa7clA2Ts,8710
|
52
|
-
MediLink/MediLink_Gmail.py,sha256=
|
52
|
+
MediLink/MediLink_Gmail.py,sha256=C6tjw4Pd1wVHbCs97cvn4GCWIvwLT2Epc5ls0OoJ3Tg,23890
|
53
53
|
MediLink/MediLink_GraphQL.py,sha256=O6OCaumT0zIC7YcIAwLOOYxiQnYhoMc48UL8ilNIBec,45720
|
54
54
|
MediLink/MediLink_Mailer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
55
55
|
MediLink/MediLink_Parser.py,sha256=w2ZD4minjwkaMz7nzP_r8v_Ow_uM5KHjpPSY8mIHcdE,9787
|
@@ -66,6 +66,8 @@ MediLink/MediLink_main.py,sha256=rvKHuZUus8RvMC5-flws2q-EvXETXiJmgU2USjzMu3g,134
|
|
66
66
|
MediLink/MediLink_smart_import.py,sha256=B5SfBn_4bYEWJJDolXbjnwKx_-MaqGZ76LyXQwWDV80,9838
|
67
67
|
MediLink/Soumit_api.py,sha256=5JfOecK98ZC6NpZklZW2AkOzkjvrbYxpJpZNH3rFxDw,497
|
68
68
|
MediLink/__init__.py,sha256=Z4Uxt4XZk4n-GwAkUoEeFiL-D7xHbttYiiWGjgKT_ng,3391
|
69
|
+
MediLink/gmail_http_utils.py,sha256=gtqCCrzJC7e8JFQzMNrf7EbK8na2h4sfTu-NMaZ_UHc,4006
|
70
|
+
MediLink/gmail_oauth_utils.py,sha256=MLuzO6awBanV7Ee2gOUrkWrxz8-Htwz2BEIFjLw9Izs,3734
|
69
71
|
MediLink/insurance_type_integration_test.py,sha256=pz2OCXitAznqDciYn6OL9M326m9CYU7YiK-ynssdQ5g,15172
|
70
72
|
MediLink/openssl.cnf,sha256=76VdcGCykf0Typyiv8Wd1mMVKixrQ5RraG6HnfKFqTo,887
|
71
73
|
MediLink/test.py,sha256=DM_E8gEbhbVfTAm3wTMiNnK2GCD1e5eH6gwTk89QIc4,3116
|
@@ -73,9 +75,9 @@ MediLink/test_cob_library.py,sha256=wUMv0-Y6fNsKcAs8Z9LwfmEBRO7oBzBAfWmmzwoNd1g,
|
|
73
75
|
MediLink/test_timing.py,sha256=yH2b8QPLDlp1Zy5AhgtjzjnDHNGhAD16ZtXtZzzESZw,2042
|
74
76
|
MediLink/test_validation.py,sha256=FJrfdUFK--xRScIzrHCg1JeGdm0uJEoRnq6CgkP2lwM,4154
|
75
77
|
MediLink/webapp.html,sha256=JPKT559aFVBi1r42Hz7C77Jj0teZZRumPhBev8eSOLk,19806
|
76
|
-
medicafe-0.
|
77
|
-
medicafe-0.
|
78
|
-
medicafe-0.
|
79
|
-
medicafe-0.
|
80
|
-
medicafe-0.
|
81
|
-
medicafe-0.
|
78
|
+
medicafe-0.250811.0.dist-info/LICENSE,sha256=65lb-vVujdQK7uMH3RRJSMwUW-WMrMEsc5sOaUn2xUk,1096
|
79
|
+
medicafe-0.250811.0.dist-info/METADATA,sha256=tnhM2Rn4jbS9mJ7VGw4ncPYfdb-Do_izp0kzOImKs0c,5501
|
80
|
+
medicafe-0.250811.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
81
|
+
medicafe-0.250811.0.dist-info/entry_points.txt,sha256=m3RBUBjr-xRwEkKJ5W4a7NlqHZP_1rllGtjZnrRqKe8,52
|
82
|
+
medicafe-0.250811.0.dist-info/top_level.txt,sha256=U6-WBJ9RCEjyIs0BlzbQq_PwedCp_IV9n1616NNV5zA,26
|
83
|
+
medicafe-0.250811.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|