medicafe 0.251027.3__py3-none-any.whl → 0.251030.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 CHANGED
@@ -732,7 +732,8 @@ echo 4. Toggle Performance Logging ^(session^)
732
732
  echo 5. Send TEST error report (email)
733
733
  echo 6. Forced MediCafe version rollback
734
734
  echo 7. Process CSV Files
735
- echo 8. Back to Main Menu
735
+ echo 8. Edit Config File (JSON Editor)
736
+ echo 9. Back to Main Menu
736
737
  echo.
737
738
  set /p tchoice=Enter your choice:
738
739
  if "%tchoice%"=="1" goto open_latest_log
@@ -742,7 +743,8 @@ if "%tchoice%"=="4" goto toggle_perf_logging
742
743
  if "%tchoice%"=="5" goto send_test_error_report
743
744
  if "%tchoice%"=="6" goto forced_version_rollback
744
745
  if "%tchoice%"=="7" goto process_csvs
745
- if "%tchoice%"=="8" goto main_menu
746
+ if "%tchoice%"=="8" goto config_editor
747
+ if "%tchoice%"=="9" goto main_menu
746
748
  echo Invalid choice. Please try again.
747
749
  pause
748
750
  goto troubleshooting_menu
@@ -876,6 +878,14 @@ echo Rollback complete. Current MediCafe version: %medicafe_version%
876
878
  pause >nul
877
879
  goto troubleshooting_menu
878
880
 
881
+ ::: Config Editor
882
+ :config_editor
883
+ cls
884
+ echo Starting Config Editor...
885
+ call "%~dp0config_editor.bat"
886
+ pause
887
+ goto troubleshooting_menu
888
+
879
889
  ::: Subroutine to display the last N lines of a file
880
890
  :tail
881
891
  ::: Usage: call :tail filename number_of_lines
MediBot/__init__.py CHANGED
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.251027.3"
22
+ __version__ = "0.251030.0"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25
 
@@ -0,0 +1,90 @@
1
+ @echo off
2
+ setlocal enabledelayedexpansion
3
+
4
+ :: Config Editor Launcher
5
+ :: Called from MediBot.bat Troubleshooting menu
6
+
7
+ :: Get script directory and set paths
8
+ set "script_dir=%~dp0"
9
+ set "workspace_root=%script_dir%.."
10
+ set "python_script=%script_dir%config_editor.py"
11
+
12
+ :: Use the same path resolution logic as MediLink_ConfigLoader
13
+ :: Try default path first (relative to MediCafe module)
14
+ set "config_path=%workspace_root%\json\config.json"
15
+
16
+ :: If default path doesn't exist, try platform-specific fallbacks
17
+ if not exist "%config_path%" (
18
+ :: Check for Windows XP (F: drive)
19
+ if exist "F:\" (
20
+ set "config_path=F:\Medibot\json\config.json"
21
+ ) else (
22
+ :: Use current working directory for other Windows versions
23
+ set "config_path=%CD%\json\config.json"
24
+ )
25
+ )
26
+
27
+ :: Display header
28
+ cls
29
+ echo ========================================
30
+ echo MediCafe Config Editor
31
+ echo ========================================
32
+ echo.
33
+
34
+ :: Check if Python is available
35
+ python --version >nul 2>&1
36
+ if errorlevel 1 (
37
+ echo ERROR: Python is not available or not in PATH
38
+ echo.
39
+ echo Please ensure Python 3.4.4 or later is installed and accessible.
40
+ echo.
41
+ pause
42
+ exit /b 1
43
+ )
44
+
45
+ :: Check if config file exists
46
+ if not exist "%config_path%" (
47
+ echo WARNING: Config file not found at:
48
+ echo %config_path%
49
+ echo.
50
+ echo The editor will start with an empty configuration.
51
+ echo You can create a new config file or navigate to the correct location.
52
+ echo.
53
+ set /p continue_choice="Continue anyway? (Y/N): "
54
+ if /i not "!continue_choice!"=="Y" (
55
+ echo Operation cancelled.
56
+ pause
57
+ exit /b 0
58
+ )
59
+ )
60
+
61
+ :: Check if editor script exists
62
+ if not exist "%python_script%" (
63
+ echo ERROR: Config editor script not found at:
64
+ echo %python_script%
65
+ echo.
66
+ echo Please ensure config_editor.py is in the MediBot directory.
67
+ echo.
68
+ pause
69
+ exit /b 1
70
+ )
71
+
72
+ :: Run the config editor
73
+ echo Starting config editor...
74
+ echo.
75
+ python "%python_script%" "%config_path%"
76
+
77
+ :: Check exit code
78
+ if errorlevel 1 (
79
+ echo.
80
+ echo ERROR: Config editor encountered an error.
81
+ echo.
82
+ ) else (
83
+ echo.
84
+ echo Config editor completed successfully.
85
+ echo.
86
+ )
87
+
88
+ :: Return to caller
89
+ pause
90
+ exit /b %errorlevel%
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ DOS-Style Config.json Editor
5
+ Interactive JSON configuration editor with DOS-like interface.
6
+ Compatible with Python 3.4.4 and Windows XP.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import json
12
+ from collections import OrderedDict
13
+
14
+ # Import our helper functions
15
+ from config_editor_helpers import (
16
+ load_config_safe, create_backup, save_config_atomic, validate_json_structure,
17
+ is_sensitive_key, get_value_type, format_value_for_display, clear_config_cache,
18
+ get_nested_value, set_nested_value, get_path_string, validate_key_name, parse_value_input,
19
+ resolve_config_path
20
+ )
21
+
22
+ class ConfigEditor:
23
+ def __init__(self, config_path):
24
+ self.config_path = config_path
25
+ self.config = OrderedDict()
26
+ self.original_config = OrderedDict()
27
+ self.current_path = [] # List of keys representing current location
28
+ self.staged_changes = [] # List of (path, old_value, new_value) tuples
29
+ self.running = True
30
+
31
+ # Load initial config
32
+ self.config, error = load_config_safe(config_path)
33
+ if error:
34
+ print("WARNING: {}".format(error))
35
+ print("Starting with empty configuration.")
36
+ self.config = OrderedDict()
37
+
38
+ # Keep a copy of original for comparison
39
+ self.original_config = json.loads(json.dumps(self.config, ensure_ascii=False), object_pairs_hook=OrderedDict)
40
+
41
+ def clear_screen(self):
42
+ """Clear the console screen (XP compatible)."""
43
+ os.system('cls' if os.name == 'nt' else 'clear')
44
+
45
+ def print_header(self):
46
+ """Print the editor header."""
47
+ print("=" * 60)
48
+ print(" MediCafe Config Editor")
49
+ print("=" * 60)
50
+ print("")
51
+
52
+ def print_breadcrumb(self):
53
+ """Print current navigation path."""
54
+ path_str = get_path_string(self.current_path)
55
+ print("Current Path: {}".format(path_str))
56
+ print("-" * 60)
57
+
58
+ def print_current_level(self):
59
+ """Print the current level of the config tree."""
60
+ current_data, found = get_nested_value(self.config, self.current_path)
61
+
62
+ if not found:
63
+ print("ERROR: Invalid path")
64
+ return
65
+
66
+ if not isinstance(current_data, dict):
67
+ print("ERROR: Cannot navigate into non-object value")
68
+ return
69
+
70
+ if not current_data:
71
+ print("(Empty object)")
72
+ return
73
+
74
+ # Print numbered items
75
+ items = list(current_data.items())
76
+ for i, (key, value) in enumerate(items, 1):
77
+ value_display = format_value_for_display(value)
78
+ value_type = get_value_type(value)
79
+
80
+ # Mark sensitive keys
81
+ sensitive_mark = " [SENSITIVE]" if is_sensitive_key(key) else ""
82
+
83
+ print("[{}] {}: {} ({}){}".format(i, key, value_display, value_type, sensitive_mark))
84
+
85
+ def print_commands(self):
86
+ """Print available commands."""
87
+ print("")
88
+ print("Commands:")
89
+ print(" [1-N] Navigate/Edit [0] Back [EDIT #] Edit [ADD] Add Key")
90
+ print(" [DIR] Refresh [HELP] Help [EXIT] Save & Exit")
91
+ print("-" * 60)
92
+
93
+ def print_help(self):
94
+ """Print detailed help information."""
95
+ self.clear_screen()
96
+ self.print_header()
97
+ print("HELP - Config Editor Commands")
98
+ print("=" * 60)
99
+ print("")
100
+ print("Navigation:")
101
+ print(" [1-N] - Navigate into nested objects or edit simple values")
102
+ print(" [0] - Go back to parent level")
103
+ print(" [..] - Same as [0]")
104
+ print("")
105
+ print("Editing:")
106
+ print(" [EDIT #] - Edit the numbered item")
107
+ print(" [ADD] - Add a new key at current level")
108
+ print("")
109
+ print("Utilities:")
110
+ print(" [DIR] - Refresh current level display")
111
+ print(" [HELP] - Show this help screen")
112
+ print(" [EXIT] - Save changes and exit")
113
+ print(" [Q] - Same as [EXIT]")
114
+ print("")
115
+ print("Safety Features:")
116
+ print(" - All changes are staged until you save")
117
+ print(" - Automatic backup created before saving")
118
+ print(" - Sensitive keys marked with [SENSITIVE]")
119
+ print(" - Preview all changes before committing")
120
+ print("")
121
+ print("Press Enter to continue...")
122
+ input()
123
+
124
+ def navigate_to_item(self, item_number):
125
+ """Navigate to a numbered item."""
126
+ current_data, found = get_nested_value(self.config, self.current_path)
127
+
128
+ if not found or not isinstance(current_data, dict):
129
+ print("ERROR: Cannot navigate from current location")
130
+ return
131
+
132
+ items = list(current_data.items())
133
+ if item_number < 1 or item_number > len(items):
134
+ print("ERROR: Invalid item number")
135
+ return
136
+
137
+ key, value = items[item_number - 1]
138
+
139
+ # If it's a simple value, offer to edit it
140
+ if not isinstance(value, dict):
141
+ self.edit_simple_value(key, value)
142
+ else:
143
+ # Navigate into the object
144
+ self.current_path.append(key)
145
+
146
+ def edit_simple_value(self, key, current_value):
147
+ """Edit a simple (non-object) value."""
148
+ current_type = get_value_type(current_value)
149
+ current_display = format_value_for_display(current_value)
150
+
151
+ print("")
152
+ print("Editing: {}".format(key))
153
+ print("Current value: {} ({})".format(current_display, current_type))
154
+ print("")
155
+
156
+ # Get new value
157
+ new_value_str = input("Enter new value: ").strip()
158
+
159
+ if not new_value_str:
160
+ print("No changes made.")
161
+ return
162
+
163
+ # Parse the new value
164
+ new_value, error = parse_value_input(new_value_str, current_type)
165
+ if error:
166
+ print("ERROR: {}".format(error))
167
+ return
168
+
169
+ # Show preview
170
+ new_display = format_value_for_display(new_value)
171
+ print("")
172
+ print("Preview: {} -> {}".format(current_display, new_display))
173
+
174
+ # Confirm change
175
+ if is_sensitive_key(key):
176
+ print("WARNING: This appears to be a sensitive key!")
177
+
178
+ confirm = input("Apply this change? (Y/N): ").strip().upper()
179
+ if confirm in ['Y', 'YES']:
180
+ # Stage the change
181
+ full_path = self.current_path + [key]
182
+ self.staged_changes.append((full_path, current_value, new_value))
183
+ set_nested_value(self.config, full_path, new_value)
184
+ print("Change staged.")
185
+ else:
186
+ print("Change cancelled.")
187
+
188
+ def add_new_key(self):
189
+ """Add a new key at the current level."""
190
+ current_data, found = get_nested_value(self.config, self.current_path)
191
+
192
+ if not found or not isinstance(current_data, dict):
193
+ print("ERROR: Cannot add key at current location")
194
+ return
195
+
196
+ print("")
197
+ print("Add New Key")
198
+ print("-" * 30)
199
+
200
+ # Get key name
201
+ while True:
202
+ key_name = input("Enter key name: ").strip()
203
+ is_valid, error = validate_key_name(key_name, current_data.keys())
204
+ if is_valid:
205
+ break
206
+ print("ERROR: {}".format(error))
207
+
208
+ # Get value type
209
+ print("")
210
+ print("Value types:")
211
+ print(" 1. string - Text value")
212
+ print(" 2. number - Numeric value")
213
+ print(" 3. boolean - true/false")
214
+ print(" 4. array - List of values")
215
+ print(" 5. object - Nested structure")
216
+ print("")
217
+
218
+ while True:
219
+ type_choice = input("Select value type (1-5): ").strip()
220
+ type_map = {'1': 'string', '2': 'number', '3': 'boolean', '4': 'array', '5': 'object'}
221
+ if type_choice in type_map:
222
+ value_type = type_map[type_choice]
223
+ break
224
+ print("ERROR: Invalid choice. Enter 1-5.")
225
+
226
+ # Get value based on type
227
+ if value_type == 'string':
228
+ value_input = input("Enter string value: ").strip()
229
+ elif value_type == 'number':
230
+ value_input = input("Enter number value: ").strip()
231
+ elif value_type == 'boolean':
232
+ value_input = input("Enter boolean value (true/false): ").strip()
233
+ elif value_type == 'array':
234
+ value_input = input("Enter array values (comma-separated): ").strip()
235
+ elif value_type == 'object':
236
+ value_input = "" # Empty object
237
+ print("Creating empty object. You can add keys to it later.")
238
+
239
+ # Parse the value
240
+ new_value, error = parse_value_input(value_input, value_type)
241
+ if error:
242
+ print("ERROR: {}".format(error))
243
+ return
244
+
245
+ # Show preview
246
+ new_display = format_value_for_display(new_value)
247
+ print("")
248
+ print("Preview: {}: {} ({})".format(key_name, new_display, value_type))
249
+
250
+ # Confirm addition
251
+ if is_sensitive_key(key_name):
252
+ print("WARNING: This appears to be a sensitive key!")
253
+
254
+ confirm = input("Add this key? (Y/N): ").strip().upper()
255
+ if confirm in ['Y', 'YES']:
256
+ # Stage the change
257
+ full_path = self.current_path + [key_name]
258
+ self.staged_changes.append((full_path, None, new_value))
259
+ set_nested_value(self.config, full_path, new_value)
260
+ print("Key added and staged.")
261
+ else:
262
+ print("Addition cancelled.")
263
+
264
+ def show_staged_changes(self):
265
+ """Show all staged changes."""
266
+ if not self.staged_changes:
267
+ print("No staged changes.")
268
+ return
269
+
270
+ print("")
271
+ print("Staged Changes:")
272
+ print("-" * 30)
273
+
274
+ for i, (path, old_value, new_value) in enumerate(self.staged_changes, 1):
275
+ path_str = " > ".join(path)
276
+ if old_value is None:
277
+ print("{}. ADD: {} = {}".format(i, path_str, format_value_for_display(new_value)))
278
+ else:
279
+ print("{}. EDIT: {} = {} -> {}".format(
280
+ i, path_str,
281
+ format_value_for_display(old_value),
282
+ format_value_for_display(new_value)
283
+ ))
284
+
285
+ def save_changes(self):
286
+ """Save all staged changes to file."""
287
+ if not self.staged_changes:
288
+ print("No changes to save.")
289
+ return True
290
+
291
+ print("")
292
+ print("Saving Changes...")
293
+ print("-" * 30)
294
+
295
+ # Show summary
296
+ self.show_staged_changes()
297
+ print("")
298
+
299
+ # Validate JSON structure
300
+ is_valid, error = validate_json_structure(self.config)
301
+ if not is_valid:
302
+ print("ERROR: {}".format(error))
303
+ return False
304
+
305
+ # Create backup
306
+ backup_path = create_backup(self.config_path)
307
+ if backup_path:
308
+ print("Backup created: {}".format(os.path.basename(backup_path)))
309
+ else:
310
+ print("WARNING: Could not create backup")
311
+
312
+ # Save to file
313
+ success, error = save_config_atomic(self.config_path, self.config)
314
+ if not success:
315
+ print("ERROR: {}".format(error))
316
+ return False
317
+
318
+ # Clear cache
319
+ cache_success, cache_error = clear_config_cache()
320
+ if not cache_success:
321
+ print("WARNING: {}".format(cache_error))
322
+
323
+ print("Configuration saved successfully!")
324
+ return True
325
+
326
+ def process_command(self, command):
327
+ """Process a user command."""
328
+ command = command.strip().upper()
329
+
330
+ if command == 'EXIT' or command == 'Q':
331
+ if self.staged_changes:
332
+ print("")
333
+ print("You have unsaved changes.")
334
+ self.show_staged_changes()
335
+ print("")
336
+ save_choice = input("Save changes before exiting? (Y/N): ").strip().upper()
337
+ if save_choice in ['Y', 'YES']:
338
+ if self.save_changes():
339
+ self.running = False
340
+ else:
341
+ print("Changes discarded.")
342
+ self.running = False
343
+ else:
344
+ self.running = False
345
+
346
+ elif command == 'HELP':
347
+ self.print_help()
348
+
349
+ elif command == 'DIR':
350
+ pass # Will refresh display
351
+
352
+ elif command == 'ADD':
353
+ self.add_new_key()
354
+
355
+ elif command.startswith('EDIT '):
356
+ try:
357
+ item_num = int(command[5:].strip())
358
+ self.navigate_to_item(item_num)
359
+ except ValueError:
360
+ print("ERROR: Invalid EDIT command. Use EDIT followed by a number.")
361
+
362
+ elif command == '0' or command == '..':
363
+ if self.current_path:
364
+ self.current_path.pop()
365
+ else:
366
+ print("Already at root level.")
367
+
368
+ elif command.isdigit():
369
+ item_num = int(command)
370
+ self.navigate_to_item(item_num)
371
+
372
+ else:
373
+ print("ERROR: Unknown command '{}'. Type HELP for available commands.".format(command))
374
+
375
+ def run(self):
376
+ """Main editor loop."""
377
+ while self.running:
378
+ self.clear_screen()
379
+ self.print_header()
380
+ self.print_breadcrumb()
381
+ self.print_current_level()
382
+ self.print_commands()
383
+
384
+ if self.staged_changes:
385
+ print("")
386
+ print("You have {} unsaved changes.".format(len(self.staged_changes)))
387
+
388
+ try:
389
+ command = input("Config> ").strip()
390
+ if command:
391
+ self.process_command(command)
392
+ except KeyboardInterrupt:
393
+ print("\n")
394
+ print("Interrupted by user.")
395
+ if self.staged_changes:
396
+ save_choice = input("Save changes before exiting? (Y/N): ").strip().upper()
397
+ if save_choice in ['Y', 'YES']:
398
+ self.save_changes()
399
+ break
400
+ except EOFError:
401
+ break
402
+
403
+ print("Config editor closed.")
404
+
405
+ def main():
406
+ """Main entry point."""
407
+ if len(sys.argv) != 2:
408
+ print("Usage: python config_editor.py <config_path>")
409
+ sys.exit(1)
410
+
411
+ # Resolve the config path using the same logic as MediLink_ConfigLoader
412
+ default_path = sys.argv[1]
413
+ config_path = resolve_config_path(default_path)
414
+
415
+ # Ensure the directory exists (create it if needed)
416
+ config_dir = os.path.dirname(config_path)
417
+ if not os.path.exists(config_dir):
418
+ try:
419
+ os.makedirs(config_dir)
420
+ except OSError as e:
421
+ # Ignore error if directory already exists (race condition)
422
+ if e.errno != 17: # EEXIST
423
+ print("ERROR: Could not create config directory '{}': {}".format(config_dir, str(e)))
424
+ sys.exit(1)
425
+
426
+ editor = ConfigEditor(config_path)
427
+ editor.run()
428
+
429
+ if __name__ == "__main__":
430
+ main()
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Config Editor Helper Functions
5
+ Provides JSON loading, validation, backup, and atomic write utilities for the config editor.
6
+ Compatible with Python 3.4.4 and Windows XP.
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import time
12
+ import tempfile
13
+ import shutil
14
+ import platform
15
+ from collections import OrderedDict
16
+
17
+ # Sensitive keys that should trigger extra confirmation
18
+ SENSITIVE_KEYS = ['password', 'token', 'secret', 'key', 'auth', 'credential', 'api_key', 'client_secret']
19
+
20
+ def resolve_config_path(default_path):
21
+ """
22
+ Resolve the config file path using the same logic as MediLink_ConfigLoader.
23
+ This ensures the editor finds the config file in the same location as the main application.
24
+
25
+ Args:
26
+ default_path: The default path to try first
27
+
28
+ Returns:
29
+ str: The resolved config file path
30
+ """
31
+ # If the default path exists, use it
32
+ if os.path.exists(default_path):
33
+ return default_path
34
+
35
+ # Try platform-specific fallbacks (same logic as MediLink_ConfigLoader)
36
+ if platform.system() == 'Windows' and platform.release() == 'XP':
37
+ # Use F: paths for Windows XP
38
+ xp_path = "F:\\Medibot\\json\\config.json"
39
+ if os.path.exists(xp_path):
40
+ return xp_path
41
+ elif platform.system() == 'Windows':
42
+ # Use current working directory for other versions of Windows
43
+ cwd_path = os.path.join(os.getcwd(), 'json', 'config.json')
44
+ if os.path.exists(cwd_path):
45
+ return cwd_path
46
+
47
+ # If no fallback paths exist, return the original default path
48
+ # (the editor will handle the missing file gracefully)
49
+ return default_path
50
+
51
+ def load_config_safe(config_path):
52
+ """
53
+ Safely load config.json with fallback to empty structure.
54
+ Returns (config_dict, error_message)
55
+ """
56
+ try:
57
+ if not os.path.exists(config_path):
58
+ return {}, "Config file not found: {}".format(config_path)
59
+
60
+ with open(config_path, 'r', encoding='utf-8') as f:
61
+ config = json.load(f, object_pairs_hook=OrderedDict)
62
+
63
+ if not isinstance(config, dict):
64
+ return {}, "Config file does not contain a valid JSON object"
65
+
66
+ return config, None
67
+
68
+ except json.JSONDecodeError as e:
69
+ return {}, "Invalid JSON in config file: {}".format(str(e))
70
+ except Exception as e:
71
+ return {}, "Error loading config file: {}".format(str(e))
72
+
73
+ def create_backup(config_path):
74
+ """
75
+ Create a timestamped backup of the config file.
76
+ Returns backup_path or None if failed.
77
+ """
78
+ try:
79
+ if not os.path.exists(config_path):
80
+ return None
81
+
82
+ # Create timestamp in format YYYYMMDD_HHMMSS
83
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
84
+ backup_path = "{}.bak.{}".format(config_path, timestamp)
85
+
86
+ shutil.copy2(config_path, backup_path)
87
+ return backup_path
88
+
89
+ except Exception as e:
90
+ print("Warning: Could not create backup: {}".format(str(e)))
91
+ return None
92
+
93
+ def save_config_atomic(config_path, config_dict):
94
+ """
95
+ Save config dictionary to file using atomic write (temp file + rename).
96
+ Compatible with Windows XP.
97
+ Returns (success, error_message)
98
+ """
99
+ try:
100
+ # Create temp file in same directory as target
101
+ temp_dir = os.path.dirname(config_path)
102
+ temp_fd, temp_path = tempfile.mkstemp(suffix='.tmp', dir=temp_dir)
103
+
104
+ try:
105
+ # Write to temp file
106
+ with os.fdopen(temp_fd, 'w', encoding='utf-8') as f:
107
+ json.dump(config_dict, f, ensure_ascii=False, indent=4, separators=(',', ': '))
108
+
109
+ # Atomic rename (works on Windows XP)
110
+ if os.name == 'nt': # Windows
111
+ if os.path.exists(config_path):
112
+ os.remove(config_path)
113
+ os.rename(temp_path, config_path)
114
+ else: # Unix-like
115
+ os.rename(temp_path, config_path)
116
+
117
+ return True, None
118
+
119
+ except Exception as e:
120
+ # Clean up temp file on error
121
+ try:
122
+ os.close(temp_fd)
123
+ if os.path.exists(temp_path):
124
+ os.remove(temp_path)
125
+ except:
126
+ pass
127
+ raise e
128
+
129
+ except Exception as e:
130
+ return False, "Error saving config: {}".format(str(e))
131
+
132
+ def validate_json_structure(data):
133
+ """
134
+ Validate that data can be serialized to valid JSON.
135
+ Returns (is_valid, error_message)
136
+ """
137
+ try:
138
+ json.dumps(data, ensure_ascii=False)
139
+ return True, None
140
+ except Exception as e:
141
+ return False, "Invalid JSON structure: {}".format(str(e))
142
+
143
+ def is_sensitive_key(key_name):
144
+ """
145
+ Check if a key name appears to be sensitive.
146
+ """
147
+ key_lower = key_name.lower()
148
+ return any(sensitive in key_lower for sensitive in SENSITIVE_KEYS)
149
+
150
+ def get_value_type(value):
151
+ """
152
+ Determine the type of a JSON value for display purposes.
153
+ """
154
+ if isinstance(value, str):
155
+ return "string"
156
+ elif isinstance(value, (int, float)):
157
+ return "number"
158
+ elif isinstance(value, bool):
159
+ return "boolean"
160
+ elif isinstance(value, list):
161
+ return "array"
162
+ elif isinstance(value, dict):
163
+ return "object"
164
+ else:
165
+ return "unknown"
166
+
167
+ def format_value_for_display(value, max_length=50):
168
+ """
169
+ Format a value for display in the editor, truncating if too long.
170
+ """
171
+ if isinstance(value, str):
172
+ if len(value) > max_length:
173
+ return '"{}..."'.format(value[:max_length-3])
174
+ else:
175
+ return '"{}"'.format(value)
176
+ elif isinstance(value, (int, float)):
177
+ return str(value)
178
+ elif isinstance(value, bool):
179
+ return "true" if value else "false"
180
+ elif isinstance(value, list):
181
+ return "[{} items]".format(len(value))
182
+ elif isinstance(value, dict):
183
+ return "{{{} keys}}".format(len(value))
184
+ else:
185
+ return str(value)
186
+
187
+ def clear_config_cache():
188
+ """
189
+ Clear the MediLink_ConfigLoader cache after config changes.
190
+ """
191
+ try:
192
+ # Import here to avoid issues if MediCafe not available
193
+ import sys
194
+ import os
195
+
196
+ # Add parent directory to path to find MediCafe
197
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
198
+ if parent_dir not in sys.path:
199
+ sys.path.insert(0, parent_dir)
200
+
201
+ from MediCafe.MediLink_ConfigLoader import clear_config_cache as clear_loader_cache
202
+
203
+ # Use the loader's own cache clearing function
204
+ clear_loader_cache()
205
+
206
+ return True, None
207
+
208
+ except Exception as e:
209
+ return False, "Warning: Could not clear config cache: {}".format(str(e))
210
+
211
+ def get_nested_value(config, path_list):
212
+ """
213
+ Get a value from nested dictionary using path list.
214
+ Returns (value, found) where found is boolean.
215
+ """
216
+ current = config
217
+ try:
218
+ for key in path_list:
219
+ current = current[key]
220
+ return current, True
221
+ except (KeyError, TypeError):
222
+ return None, False
223
+
224
+ def set_nested_value(config, path_list, value):
225
+ """
226
+ Set a value in nested dictionary using path list.
227
+ Creates intermediate dictionaries if needed.
228
+ """
229
+ current = config
230
+ for key in path_list[:-1]:
231
+ if key not in current or not isinstance(current[key], dict):
232
+ current[key] = OrderedDict()
233
+ current = current[key]
234
+ current[path_list[-1]] = value
235
+
236
+ def get_path_string(path_list):
237
+ """
238
+ Convert path list to breadcrumb string.
239
+ """
240
+ if not path_list:
241
+ return "config"
242
+ return "config > " + " > ".join(path_list)
243
+
244
+ def validate_key_name(key_name, existing_keys):
245
+ """
246
+ Validate a new key name.
247
+ Returns (is_valid, error_message)
248
+ """
249
+ if not key_name:
250
+ return False, "Key name cannot be empty"
251
+
252
+ if key_name in existing_keys:
253
+ return False, "Key '{}' already exists".format(key_name)
254
+
255
+ # Check for invalid characters that might break JSON
256
+ invalid_chars = ['"', '\\', '/', '\n', '\r', '\t']
257
+ for char in invalid_chars:
258
+ if char in key_name:
259
+ return False, "Key name contains invalid character: '{}'".format(char)
260
+
261
+ return True, None
262
+
263
+ def parse_value_input(value_str, value_type):
264
+ """
265
+ Parse user input string into appropriate Python type.
266
+ Returns (parsed_value, error_message)
267
+ """
268
+ try:
269
+ if value_type == "string":
270
+ # Remove surrounding quotes if present
271
+ if value_str.startswith('"') and value_str.endswith('"'):
272
+ value_str = value_str[1:-1]
273
+ return value_str, None
274
+
275
+ elif value_type == "number":
276
+ # Try int first, then float
277
+ if '.' in value_str:
278
+ return float(value_str), None
279
+ else:
280
+ return int(value_str), None
281
+
282
+ elif value_type == "boolean":
283
+ value_lower = value_str.lower()
284
+ if value_lower in ['true', 't', 'yes', 'y', '1']:
285
+ return True, None
286
+ elif value_lower in ['false', 'f', 'no', 'n', '0']:
287
+ return False, None
288
+ else:
289
+ return None, "Invalid boolean value. Use true/false, yes/no, or 1/0"
290
+
291
+ elif value_type == "array":
292
+ # Simple array parsing - comma separated values
293
+ if not value_str.strip():
294
+ return [], None
295
+
296
+ # Split by comma and strip whitespace
297
+ items = [item.strip() for item in value_str.split(',')]
298
+ return items, None
299
+
300
+ elif value_type == "object":
301
+ # For now, return empty object - user can add keys later
302
+ return OrderedDict(), None
303
+
304
+ else:
305
+ return None, "Unknown value type: {}".format(value_type)
306
+
307
+ except ValueError as e:
308
+ return None, "Invalid value format: {}".format(str(e))
309
+ except Exception as e:
310
+ return None, "Error parsing value: {}".format(str(e))
MediCafe/__init__.py CHANGED
@@ -27,7 +27,7 @@ Smart Import System:
27
27
  api_suite = get_api_access()
28
28
  """
29
29
 
30
- __version__ = "0.251027.3"
30
+ __version__ = "0.251030.0"
31
31
  __author__ = "Daniel Vidaud"
32
32
  __email__ = "daniel@personalizedtransformation.com"
33
33
 
@@ -458,10 +458,19 @@ def open_browser_with_executable(url, browser_path=None):
458
458
  def initiate_link_retrieval(config):
459
459
  log("Initiating browser via implicit GET.")
460
460
  medi = extract_medilink_config(config)
461
- url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(medi.get('webapp_deployment_id', ''))
461
+ dep_id = (medi.get('webapp_deployment_id', '') or '').strip()
462
+ if not dep_id:
463
+ log("webapp_deployment_id is empty. Please set it in config before continuing.")
464
+ shutdown_event.set()
465
+ return
466
+ url_get = "https://script.google.com/macros/s/{}/exec?action=get_link".format(dep_id)
467
+ try:
468
+ log("Opening GAS web app: {}".format(url_get))
469
+ except Exception:
470
+ pass
462
471
  open_browser_with_executable(url_get)
463
472
  log("Preparing POST call.")
464
- url = "https://script.google.com/macros/s/{}/exec".format(medi.get('webapp_deployment_id', ''))
473
+ url = "https://script.google.com/macros/s/{}/exec".format(dep_id)
465
474
  downloaded_emails = list(load_downloaded_emails())
466
475
  payload = {"downloadedEmails": downloaded_emails}
467
476
  access_token = get_access_token()
MediLink/__init__.py CHANGED
@@ -22,7 +22,7 @@ Smart Import Integration:
22
22
  datamgmt = get_components('medilink_datamgmt')
23
23
  """
24
24
 
25
- __version__ = "0.251027.3"
25
+ __version__ = "0.251030.0"
26
26
  __author__ = "Daniel Vidaud"
27
27
  __email__ = "daniel@personalizedtransformation.com"
28
28
 
MediLink/webapp.html CHANGED
@@ -177,7 +177,13 @@
177
177
  </main>
178
178
  <script>
179
179
  document.addEventListener("DOMContentLoaded", () => {
180
- console.log("Document ready, initializing flows...");
180
+ try {
181
+ var params = new URLSearchParams(window.location.search || '');
182
+ var actionParam = params.get('action');
183
+ console.log("Document ready, initializing flows... action=", actionParam);
184
+ } catch (e) {
185
+ console.log("Document ready, initializing flows...");
186
+ }
181
187
  // initializeEmailListFlow();
182
188
  initializeDocxEmailListFlow();
183
189
  });
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: medicafe
3
- Version: 0.251027.3
3
+ Version: 0.251030.0
4
4
  Summary: MediCafe
5
5
  Home-page: https://github.com/katanada2/MediCafe
6
6
  Author: Daniel Vidaud
@@ -1,4 +1,4 @@
1
- MediBot/MediBot.bat,sha256=mBoD0Dit4Om_iGgSbSoVZ0d0kScGWofA7qaYqhlJhWo,29161
1
+ MediBot/MediBot.bat,sha256=z8LmLbqdi_CmpUSN5EBDFl29Xe9sgFBjCgewW4-Wq54,29380
2
2
  MediBot/MediBot.py,sha256=ABSqWikb_c1VSuR4n8Vh5YfOqzDCi2jnnp3sg_xOYg0,51092
3
3
  MediBot/MediBot_Charges.py,sha256=a28if_f_IoazIHiqlaFosFnfEgEoCwb9LQ6aOyk5-D0,10704
4
4
  MediBot/MediBot_Crosswalk_Library.py,sha256=6LrpRx2UKVeH3TspS9LpR93iw5M7nTqN6IYpC-6PPGE,26060
@@ -12,8 +12,11 @@ MediBot/MediBot_dataformat_library.py,sha256=D46fdPtxcgfWTzaLBtSvjtozzZBNqNiODgu
12
12
  MediBot/MediBot_debug.bat,sha256=F5Lfi3nFEEo4Ddx9EbX94u3fNAMgzMp3wsn-ULyASTM,6017
13
13
  MediBot/MediBot_docx_decoder.py,sha256=9BSjV-kB90VHnqfL_5iX4zl5u0HcHvHuL7YNfx3gXpQ,33143
14
14
  MediBot/MediBot_smart_import.py,sha256=Emvz7NwemHGCHvG5kZcUyXMcCheidbGKaPfOTg-YCEs,6684
15
- MediBot/__init__.py,sha256=W7JANBDt-VBMAzdtL-KXgJ3-ODNLji-8stnIn38LUIQ,3192
15
+ MediBot/__init__.py,sha256=XbpIvNejZ4LpOh_pRbEyb46jxmqGfzyZAtyRTfAPK0k,3192
16
16
  MediBot/clear_cache.bat,sha256=F6-VhETWw6xDdGWG2wUqvtXjCl3lY4sSUFqF90bM8-8,1860
17
+ MediBot/config_editor.bat,sha256=gwR_GP5a72vhrij6U8e1jH48xQDwLgLY3vEJI9KZtcs,2340
18
+ MediBot/config_editor.py,sha256=fwFMhHA7xmrHUW_V7XY7HfjWeY19BFje1Z-D0mK70zA,15788
19
+ MediBot/config_editor_helpers.py,sha256=7jsBg9NpDla09EuT0nvKgk4EmIEbyH8vma92lfk88uM,10510
17
20
  MediBot/crash_diagnostic.bat,sha256=j8kUtyBg6NOWbXpeFuEqIRHOkVzgUrLOqO3FBMfNxTo,9268
18
21
  MediBot/f_drive_diagnostic.bat,sha256=4572hZaiwZ5wVAarPcZJQxkOSTwAdDuT_X914noARak,6878
19
22
  MediBot/full_debug_suite.bat,sha256=b3Euz-VbRaPE6tWmgpjApfv4_cB5-Rk8BLNlLrp2bpo,2223
@@ -22,7 +25,7 @@ MediBot/process_csvs.bat,sha256=3tI7h1z9eRj8rUUL4wJ7dy-Qrak20lRmpAPtGbUMbVQ,3489
22
25
  MediBot/update_json.py,sha256=vvUF4mKCuaVly8MmoadDO59M231fCIInc0KI1EtDtPA,3704
23
26
  MediBot/update_medicafe.py,sha256=G1lyvVOHYuho1d-TJQNN6qaB4HBWaJ2PpXqemBoPlRQ,17937
24
27
  MediCafe/MediLink_ConfigLoader.py,sha256=heTbZ0ItwlpxqbAb0oV2dbTFRSTMnZyhBT9fS8nI0YU,12735
25
- MediCafe/__init__.py,sha256=56u8QPxfilu-bGcU5MF8kleMtvRMpsOVEQ1VKKlZai4,5721
28
+ MediCafe/__init__.py,sha256=R7IiwM-8skk7HPc2qpkE1U8pVyUt7IreovdKhSfYkMA,5721
26
29
  MediCafe/__main__.py,sha256=NZMgsjuXYF39YS7mCWMjTO0vYf_0IAotQpNyUXfbahs,13233
27
30
  MediCafe/api_core.py,sha256=wLAdRNZdmovKReXvzsmAgKrbYon4-wbJbGCyOm_C3AU,89896
28
31
  MediCafe/api_factory.py,sha256=I5AeJoyu6m7oCrjc2OvVvO_4KSBRutTsR1riiWhTZV0,12086
@@ -51,7 +54,7 @@ MediLink/MediLink_Deductible.py,sha256=opGa5YQ6tfowurlf8xDWRtAtQMmoNYout0gYe3R5f
51
54
  MediLink/MediLink_Deductible_Validator.py,sha256=x6tHJOi88TblUpDPSH6QhIdXXRgr3rXI7kYPVGZYCgU,24998
52
55
  MediLink/MediLink_Display_Utils.py,sha256=MonsX6VPbdvqwY_V8sHUYrXCS0fMKc4toJvG0oyr-V4,24872
53
56
  MediLink/MediLink_Down.py,sha256=s4_z-RaqHYanjwbQCl-OSkg4XIpcIQ2Q6jXa8-q_QXw,28111
54
- MediLink/MediLink_Gmail.py,sha256=Gnc-tI8ACeFaw0YVPp5JUBAXhwYfptY8QGxShJu4Pbg,31476
57
+ MediLink/MediLink_Gmail.py,sha256=JuDyO8mckxwRtO8Jfty37lpbgTIA91x8LpEb6y9l_iU,31742
55
58
  MediLink/MediLink_Mailer.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
59
  MediLink/MediLink_Parser.py,sha256=eRVZ4ckZ5gDOrcvtCUZP3DOd3Djly66rCIk0aYXLz14,12567
57
60
  MediLink/MediLink_PatientProcessor.py,sha256=9r2w4p45d30Tn0kbXL3j5574MYOehP83tDirNOw_Aek,19977
@@ -63,15 +66,15 @@ MediLink/MediLink_insurance_utils.py,sha256=g741Fj2K26cMy0JX5d_XavMw9LgkK6hjaUJY
63
66
  MediLink/MediLink_main.py,sha256=1nmDtbJ9IfPwLnFhCZvDtBPSG_8gzj6LneuoV60l4IQ,26736
64
67
  MediLink/MediLink_smart_import.py,sha256=ZUXvAkIA2Pk2uuyLZazKfKK8YGdkZt1VAeZo_ZSUyxk,9942
65
68
  MediLink/Soumit_api.py,sha256=5JfOecK98ZC6NpZklZW2AkOzkjvrbYxpJpZNH3rFxDw,497
66
- MediLink/__init__.py,sha256=I6BiRTvpQ5QhK_7dLbb_4VFtyYWZf1nH2dLX1OYtgr0,3888
69
+ MediLink/__init__.py,sha256=OIl0UcNgWUpkZClnyh099Hq_sTH6TaddlcY7qpK66oM,3888
67
70
  MediLink/gmail_http_utils.py,sha256=mYChIhkbA1oJaAJA-nY3XgHQY-H7zvZJUZPhUagomsI,4047
68
71
  MediLink/gmail_oauth_utils.py,sha256=Ugr-DEqs4_RddRMSCJ_dbgA3TVeaxpbAor-dktcTIgY,3713
69
72
  MediLink/openssl.cnf,sha256=76VdcGCykf0Typyiv8Wd1mMVKixrQ5RraG6HnfKFqTo,887
70
73
  MediLink/test.py,sha256=DM_E8gEbhbVfTAm3wTMiNnK2GCD1e5eH6gwTk89QIc4,3116
71
- MediLink/webapp.html,sha256=DwDYjVvluGJ7eDdvEogfKN4t24ZJRoIUuSBfCYCL-3w,21252
72
- medicafe-0.251027.3.dist-info/LICENSE,sha256=65lb-vVujdQK7uMH3RRJSMwUW-WMrMEsc5sOaUn2xUk,1096
73
- medicafe-0.251027.3.dist-info/METADATA,sha256=xsE3H4MHme5SYio00FfRVaSBO51wYxwQIC190GL8_7Y,3414
74
- medicafe-0.251027.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
75
- medicafe-0.251027.3.dist-info/entry_points.txt,sha256=m3RBUBjr-xRwEkKJ5W4a7NlqHZP_1rllGtjZnrRqKe8,52
76
- medicafe-0.251027.3.dist-info/top_level.txt,sha256=U6-WBJ9RCEjyIs0BlzbQq_PwedCp_IV9n1616NNV5zA,26
77
- medicafe-0.251027.3.dist-info/RECORD,,
74
+ MediLink/webapp.html,sha256=SSBuPCtdv7dWj_ZiMfkCj7LRIjma6YHVJMDHjoBIdyk,21547
75
+ medicafe-0.251030.0.dist-info/LICENSE,sha256=65lb-vVujdQK7uMH3RRJSMwUW-WMrMEsc5sOaUn2xUk,1096
76
+ medicafe-0.251030.0.dist-info/METADATA,sha256=oOPTKfF8Iw-0JSxfpB6tGqSJQpGuLTASjKi2k43ZQp4,3414
77
+ medicafe-0.251030.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
78
+ medicafe-0.251030.0.dist-info/entry_points.txt,sha256=m3RBUBjr-xRwEkKJ5W4a7NlqHZP_1rllGtjZnrRqKe8,52
79
+ medicafe-0.251030.0.dist-info/top_level.txt,sha256=U6-WBJ9RCEjyIs0BlzbQq_PwedCp_IV9n1616NNV5zA,26
80
+ medicafe-0.251030.0.dist-info/RECORD,,