datasaur-dev commited on
Commit
5b4648d
·
verified ·
1 Parent(s): f97ef15

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +373 -183
app.py CHANGED
@@ -1,77 +1,129 @@
 
 
 
 
 
1
  import os
2
  import json
 
 
 
 
 
3
  import gradio as gr
4
  import pandas as pd
5
  from python_request import process_wod_document
6
-
7
- import time
8
  from dummy import output_test
9
 
10
- # --- Authentication Function ---
11
- def authenticate_user(username, password):
12
- """
13
- Simple authentication function.
14
- In production, you should use more secure methods like hashed passwords.
15
- """
16
- return username == "demo" and password == os.environ["PASSWORD"]
17
 
18
- # --- Core Application Logic ---
19
- def analyze_wod(file_obj, wod_type):
20
- """
21
- This function analyzes a Work Order Document using the remote API.
 
 
 
 
 
22
 
23
- Args:
24
- file_obj: The uploaded file object from Gradio.
25
- wod_type: The selected type of Work Order Document.
26
 
27
- Returns:
28
- A tuple containing: prediction_display, dataframe, and button_update
29
- """
30
- # Check if user has selected a valid WOD type
31
- if wod_type == "-- WOD type --" or wod_type is None:
32
- # Show warning dialog and return empty DataFrame
33
- gr.Warning("Please select a WOD type first!")
34
- return "", pd.DataFrame(), gr.update(value="Analyze Document", interactive=True)
35
-
36
- # Check if file is uploaded
37
- if file_obj is None:
38
- gr.Warning("Please upload a PDF file first!")
39
- return "", pd.DataFrame(), gr.update(value="Analyze Document", interactive=True)
40
-
41
- print(f"Analyzing '{file_obj.name}' (Type: {wod_type})...")
42
-
43
- try:
44
- # In modern Gradio versions, file_obj is already a path string
45
- # We can use it directly or get the path from it
46
- if hasattr(file_obj, 'name') and os.path.isfile(file_obj.name):
47
- # file_obj has a .name attribute pointing to the temporary file
48
- temp_file_path = file_obj.name
49
- cleanup_needed = False
50
- else:
51
- # Fallback: assume file_obj is a path string
52
- temp_file_path = str(file_obj)
53
- cleanup_needed = False
54
-
55
- # Process the document using the API
56
- api_response = process_wod_document(temp_file_path, wod_type)
57
- # time.sleep(5)
58
- # api_response = json.loads(output_test)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- # Clean up temporary file if we created one
61
- if cleanup_needed:
62
- os.unlink(temp_file_path)
63
 
64
- # Check if API call was successful
 
 
 
 
 
 
 
 
 
 
 
65
  if api_response.get("status") != "success":
66
  error_msg = api_response.get("message", "Unknown error occurred")
67
- gr.Error(f"API Error: {error_msg}")
68
- return "", pd.DataFrame(), gr.update(value="Analyze Document", interactive=True)
 
 
 
 
 
 
69
 
70
  # Parse the API response
71
  results = api_response.get("results", {})
72
  summary = results.get("summary", {})
 
 
 
 
73
 
74
- # Convert API response to DataFrame format
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  requirements = []
76
  reasons = []
77
  statuses = []
@@ -79,6 +131,7 @@ def analyze_wod(file_obj, wod_type):
79
  for requirement_name, details in summary.items():
80
  requirements.append(requirement_name)
81
  reasons.append(details.get("reasoning", ""))
 
82
  # Convert true/false to PASS/FAIL
83
  status_bool = details.get("status", "false")
84
  if isinstance(status_bool, str):
@@ -87,148 +140,285 @@ def analyze_wod(file_obj, wod_type):
87
  status = "PASS" if status_bool else "FAIL"
88
  statuses.append(status)
89
 
90
- # Create DataFrame
91
- df = pd.DataFrame({
92
  "Requirement": requirements,
93
  "Reason": reasons,
94
  "Status": statuses
95
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- # Get prediction for display
98
- prediction = results.get("prediction", "Unknown")
99
- gr.Info(f"Analysis completed! Overall prediction: {prediction}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- # Format prediction as centered H1 for display
102
- prediction_display = f"<h1 style='text-align: center; color: #1f77b4;'>{prediction}</h1>" if prediction != "Unknown" else ""
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- # Return with Reset button
105
- return prediction_display, df, gr.update(value="Reset", interactive=True)
 
106
 
107
- except Exception as e:
108
- error_msg = f"Error processing document: {str(e)}"
109
- print(error_msg)
110
- gr.Error(error_msg)
111
- return "", pd.DataFrame(), gr.update(value="Analyze Document", interactive=True)
 
 
 
 
 
 
 
 
 
 
112
 
113
- def reset_app():
 
 
 
 
 
 
 
 
 
 
 
114
  """
115
- Reset function that refreshes the page using JavaScript
 
 
 
 
 
 
 
 
116
  """
117
- return gr.update(value="Analyze Document", interactive=True)
118
 
119
- js_func = """
120
- function refresh() {
121
- const url = new URL(window.location);
122
 
123
- if (url.searchParams.get('__theme') !== 'light') {
124
- url.searchParams.set('__theme', 'light');
125
- window.location.href = url.href;
126
- }
127
- }
128
- """
129
-
130
- # JavaScript function to refresh the page when Reset is clicked
131
- refresh_js = """
132
- function(button_value) {
133
- if (button_value === "Reset") {
134
- window.location.reload();
135
- return false; // Prevent default action
136
- }
137
- return true;
138
- }
139
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- # --- Gradio User Interface Definition ---
142
- # Using gr.Blocks() for a custom layout that matches the elegant design.
143
- with gr.Blocks(
144
- #theme=gr.themes.Soft(primary_hue="blue", secondary_hue="sky"),
145
- theme=gr.themes.Default(primary_hue="blue", secondary_hue="sky"),
146
- js=js_func,
147
- css=".gradio-container {max-width: 960px !important; margin: auto !important;} .progress-text { display: none !important; }"
148
- ) as demo:
149
-
150
- # Main Title and Description
151
- gr.Markdown(
152
- """
153
- # WOD Analyzer
154
- Upload a Work Order Document to automatically check for requirements.
155
- """
156
- )
157
-
158
- # Input Section
159
- with gr.Row():
160
- # File Upload Component
161
- file_input = gr.File(label="Upload WOD PDF")
162
-
163
- # Dropdown for WOD Type
164
- type_input = gr.Dropdown(
165
- ["-- WOD type --", "REPLACEMENT", "THERMAL", "VISIT", "PREVENTIVE_MAINTENANCE", "INSTALLATION", "WITHDRAWAL"],
166
- label="Type",
167
- value="-- WOD type --",
168
- info="Select the type of work order."
169
- )
170
 
171
- # Action Button
172
- analyze_btn = gr.Button("Analyze Document", variant="primary")
173
-
174
- # Results Section
175
- gr.Markdown("---")
176
- gr.Markdown("## Results")
177
-
178
- # Prediction display (centered H1)
179
- prediction_output = gr.Markdown(value="", visible=True)
180
-
181
- # DataFrame to display the output, with styling for the 'Status' column
182
- results_output = gr.DataFrame(
183
- headers=["Requirement", "Reason", "Status"],
184
- datatype=["str", "str", "str"],
185
- interactive=False,
186
- max_height=1250,
187
- column_widths=[30, 60, 10],
188
- wrap=True
189
- )
190
-
191
- # Define the interaction: clicking the button calls the function
192
- def handle_button_click(file_obj, wod_type, current_button_value):
193
- """Handle button click with state management"""
194
- # Check if this is a reset action (this shouldn't be reached due to JS)
195
- if current_button_value == "Reset":
196
- return "", pd.DataFrame(), gr.update(value="Analyze Document", interactive=True)
197
-
198
- # Otherwise, this is an analyze action
199
- prediction_display, df, button_update = analyze_wod(file_obj, wod_type)
200
- return prediction_display, df, button_update
201
-
202
- # Set button to Processing state when clicked (only if not Reset)
203
- def set_processing_state(current_button_value):
204
- if current_button_value == "Reset":
205
- return gr.update() # Don't change if it's reset
206
- return gr.update(value="Processing...", interactive=False)
207
-
208
- # Chain the events: first set processing state, then analyze
209
- analyze_btn.click(
210
- fn=set_processing_state,
211
- inputs=[analyze_btn],
212
- outputs=[analyze_btn],
213
- show_progress=False,
214
- js=refresh_js # Add JavaScript to handle reset
215
- ).then(
216
- fn=handle_button_click,
217
- inputs=[file_input, type_input, analyze_btn],
218
- outputs=[prediction_output, results_output, analyze_btn],
219
- show_progress=True
220
- )
221
-
222
- # --- Launch the Application with Authentication ---
223
- if __name__ == "__main__":
224
- # The launch() command creates a web server with authentication enabled
225
- # Users must provide the correct username and password to access the app
226
 
227
- # demo.launch(debug=True)
228
 
229
- demo.launch(
230
- auth=authenticate_user, # Enable authentication
231
- auth_message="Please enter your credentials to access the WOD Analyzer",
232
- share=True,
233
- ssr_mode=False,
234
- )
 
1
+ """
2
+ WOD Analyzer - Clean and Refactored Version
3
+ A Gradio application for analyzing Work Order Documents with improved code structure.
4
+ """
5
+
6
  import os
7
  import json
8
+ import time
9
+ from typing import Tuple, Dict, Any, Optional, List
10
+ from dataclasses import dataclass
11
+ from enum import Enum
12
+
13
  import gradio as gr
14
  import pandas as pd
15
  from python_request import process_wod_document
 
 
16
  from dummy import output_test
17
 
18
+ PRODUCTION = True
 
 
 
 
 
 
19
 
20
+ # === CONSTANTS ===
21
+ class WODType(Enum):
22
+ """Enum for Work Order Document types."""
23
+ REPLACEMENT = "REPLACEMENT"
24
+ THERMAL = "THERMAL"
25
+ VISIT = "VISIT"
26
+ PREVENTIVE_MAINTENANCE = "PREVENTIVE_MAINTENANCE"
27
+ INSTALLATION = "INSTALLATION"
28
+ WITHDRAWAL = "WITHDRAWAL"
29
 
 
 
 
30
 
31
+ class UIConstants:
32
+ """UI-related constants."""
33
+ DEFAULT_WOD_TYPE = "-- WOD type --"
34
+ BUTTON_ANALYZE = "Analyze Document"
35
+ BUTTON_PROCESSING = "Processing..."
36
+ BUTTON_RESET = "Reset"
37
+
38
+ # Styling
39
+ MAX_WIDTH = "960px"
40
+ TABLE_MAX_HEIGHT = 1250
41
+ COLUMN_WIDTHS = [30, 60, 10]
42
+
43
+ # Messages
44
+ TITLE = "# WOD Analyzer"
45
+ DESCRIPTION = "Upload a Work Order Document to automatically check for requirements."
46
+ NO_WOD_TYPE_WARNING = "Please select a WOD type first!"
47
+ NO_FILE_WARNING = "Please upload a PDF file first!"
48
+
49
+
50
+ class Config:
51
+ """Application configuration."""
52
+ USERNAME = "demo"
53
+ PASSWORD_ENV_VAR = os.environ["PASSWORD"]
54
+ DEBUG = True
55
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
56
+
57
+
58
+ @dataclass
59
+ class AnalysisResult:
60
+ """Data class for analysis results."""
61
+ prediction: str
62
+ dataframe: pd.DataFrame
63
+ json_data: Dict[str, Any]
64
+ accordion_visible: bool
65
+ success: bool
66
+ error_message: Optional[str] = None
67
+
68
+
69
+ # === CORE BUSINESS LOGIC ===
70
+ class WODAnalyzer:
71
+ """Core business logic for WOD analysis."""
72
+
73
+ @staticmethod
74
+ def validate_inputs(file_obj: Optional[Any], wod_type: str) -> Tuple[bool, str]:
75
+ """Validate user inputs."""
76
+ if wod_type == UIConstants.DEFAULT_WOD_TYPE or wod_type is None:
77
+ return False, UIConstants.NO_WOD_TYPE_WARNING
78
 
79
+ if file_obj is None:
80
+ return False, UIConstants.NO_FILE_WARNING
 
81
 
82
+ return True, ""
83
+
84
+ @staticmethod
85
+ def get_file_path(file_obj: Any) -> str:
86
+ """Extract file path from Gradio file object."""
87
+ if hasattr(file_obj, 'name') and os.path.isfile(file_obj.name):
88
+ return file_obj.name
89
+ return str(file_obj)
90
+
91
+ @staticmethod
92
+ def process_api_response(api_response: Dict[str, Any]) -> AnalysisResult:
93
+ """Process API response and convert to AnalysisResult."""
94
  if api_response.get("status") != "success":
95
  error_msg = api_response.get("message", "Unknown error occurred")
96
+ return AnalysisResult(
97
+ prediction="",
98
+ dataframe=pd.DataFrame(),
99
+ json_data={},
100
+ accordion_visible=False,
101
+ success=False,
102
+ error_message=f"API Error: {error_msg}"
103
+ )
104
 
105
  # Parse the API response
106
  results = api_response.get("results", {})
107
  summary = results.get("summary", {})
108
+ extracted_data = api_response.get("extracted_data", {})
109
+
110
+ # Convert summary to DataFrame
111
+ df = WODAnalyzer._create_summary_dataframe(summary)
112
 
113
+ # Get prediction
114
+ prediction = results.get("prediction", "Unknown")
115
+
116
+ return AnalysisResult(
117
+ prediction=prediction,
118
+ dataframe=df,
119
+ json_data=extracted_data,
120
+ accordion_visible=bool(extracted_data),
121
+ success=True
122
+ )
123
+
124
+ @staticmethod
125
+ def _create_summary_dataframe(summary: Dict[str, Any]) -> pd.DataFrame:
126
+ """Create DataFrame from summary data."""
127
  requirements = []
128
  reasons = []
129
  statuses = []
 
131
  for requirement_name, details in summary.items():
132
  requirements.append(requirement_name)
133
  reasons.append(details.get("reasoning", ""))
134
+
135
  # Convert true/false to PASS/FAIL
136
  status_bool = details.get("status", "false")
137
  if isinstance(status_bool, str):
 
140
  status = "PASS" if status_bool else "FAIL"
141
  statuses.append(status)
142
 
143
+ return pd.DataFrame({
 
144
  "Requirement": requirements,
145
  "Reason": reasons,
146
  "Status": statuses
147
  })
148
+
149
+ @classmethod
150
+ def analyze_document(cls, file_obj: Any, wod_type: str) -> AnalysisResult:
151
+ """Main analysis function."""
152
+ # Validate inputs
153
+ is_valid, error_msg = cls.validate_inputs(file_obj, wod_type)
154
+ if not is_valid:
155
+ return AnalysisResult(
156
+ prediction="",
157
+ dataframe=pd.DataFrame(),
158
+ json_data={},
159
+ accordion_visible=False,
160
+ success=False,
161
+ error_message=error_msg
162
+ )
163
 
164
+ try:
165
+ print(f"Analyzing '{file_obj.name}' (Type: {wod_type})...")
166
+
167
+ # Get file path
168
+ file_path = cls.get_file_path(file_obj)
169
+
170
+ # Process the document using the API (currently mocked)
171
+ #
172
+
173
+ if PRODUCTION:
174
+ api_response = process_wod_document(file_path, wod_type)
175
+ else:
176
+ time.sleep(1) # Simulate processing time
177
+ api_response = json.loads(output_test)
178
+
179
+ # Process the response
180
+ return cls.process_api_response(api_response)
181
+
182
+ except Exception as e:
183
+ error_msg = f"Error processing document: {str(e)}"
184
+ print(error_msg)
185
+ return AnalysisResult(
186
+ prediction="",
187
+ dataframe=pd.DataFrame(),
188
+ json_data={},
189
+ accordion_visible=False,
190
+ success=False,
191
+ error_message=error_msg
192
+ )
193
+
194
+
195
+ # === AUTHENTICATION ===
196
+ class AuthManager:
197
+ """Handle user authentication."""
198
+
199
+ @staticmethod
200
+ def authenticate_user(username: str, password: str) -> bool:
201
+ """
202
+ Simple authentication function.
203
+ In production, use more secure methods like hashed passwords.
204
+ """
205
+ expected_password = Config.PASSWORD_ENV_VAR
206
+ if not expected_password:
207
+ print("Warning: PASSWORD environment variable not set")
208
+ return False
209
+
210
+ return username == Config.USERNAME and password == expected_password
211
+
212
+
213
+ # === UI COMPONENTS ===
214
+ class UIBuilder:
215
+ """Builds and manages UI components."""
216
+
217
+ @staticmethod
218
+ def get_wod_type_options() -> List[str]:
219
+ """Get WOD type dropdown options."""
220
+ return [UIConstants.DEFAULT_WOD_TYPE] + [wod_type.value for wod_type in WODType]
221
+
222
+ @staticmethod
223
+ def create_custom_css() -> str:
224
+ """Create custom CSS for the application."""
225
+ return f"""
226
+ .gradio-container {{
227
+ max-width: {UIConstants.MAX_WIDTH} !important;
228
+ margin: auto !important;
229
+ }}
230
+ .progress-text {{
231
+ display: none !important;
232
+ }}
233
+ """
234
+
235
+ @staticmethod
236
+ def create_theme() -> gr.Theme:
237
+ """Create custom theme for the application."""
238
+ return gr.themes.Default(primary_hue="blue", secondary_hue="sky")
239
+
240
+ @staticmethod
241
+ def format_prediction_display(prediction: str) -> str:
242
+ """Format prediction for display."""
243
+ if prediction and prediction != "Unknown":
244
+ return f"<h1 style='text-align: center; color: #1f77b4;'>{prediction}</h1>"
245
+ return ""
246
+
247
+
248
+ # === EVENT HANDLERS ===
249
+ class EventHandlers:
250
+ """Handle UI events and interactions."""
251
+
252
+ @staticmethod
253
+ def handle_analyze_button(
254
+ file_obj: Any,
255
+ wod_type: str,
256
+ current_button_value: str
257
+ ) -> Tuple[str, pd.DataFrame, Dict[str, Any], gr.update, gr.update]:
258
+ """Handle analyze button click."""
259
+ # Check if this is a reset action
260
+ if current_button_value == UIConstants.BUTTON_RESET:
261
+ return (
262
+ "",
263
+ pd.DataFrame(),
264
+ {},
265
+ gr.update(visible=False),
266
+ gr.update(value=UIConstants.BUTTON_ANALYZE, interactive=True)
267
+ )
268
+
269
+ # Perform analysis
270
+ result = WODAnalyzer.analyze_document(file_obj, wod_type)
271
 
272
+ if not result.success:
273
+ if result.error_message:
274
+ if "Please select" in result.error_message or "Please upload" in result.error_message:
275
+ gr.Warning(result.error_message)
276
+ else:
277
+ gr.Error(result.error_message)
278
+
279
+ return (
280
+ "",
281
+ pd.DataFrame(),
282
+ {},
283
+ gr.update(visible=False),
284
+ gr.update(value=UIConstants.BUTTON_ANALYZE, interactive=True)
285
+ )
286
 
287
+ # Success case
288
+ gr.Info(f"Analysis completed! Overall prediction: {result.prediction}")
289
+ prediction_display = UIBuilder.format_prediction_display(result.prediction)
290
 
291
+ return (
292
+ prediction_display,
293
+ result.dataframe,
294
+ result.json_data,
295
+ gr.update(visible=result.accordion_visible),
296
+ gr.update(value=UIConstants.BUTTON_RESET, interactive=True)
297
+ )
298
+
299
+ @staticmethod
300
+ def set_processing_state(current_button_value: str) -> gr.update:
301
+ """Set button to processing state."""
302
+ if current_button_value == UIConstants.BUTTON_RESET:
303
+ return gr.update() # Don't change if it's reset
304
+ return gr.update(value=UIConstants.BUTTON_PROCESSING, interactive=False)
305
+
306
 
307
+ # === JAVASCRIPT FUNCTIONS ===
308
+ class JavaScriptFunctions:
309
+ """JavaScript functions for the UI."""
310
+
311
+ THEME_SETUP = """
312
+ function refresh() {
313
+ const url = new URL(window.location);
314
+ if (url.searchParams.get('__theme') !== 'light') {
315
+ url.searchParams.set('__theme', 'light');
316
+ window.location.href = url.href;
317
+ }
318
+ }
319
  """
320
+
321
+ REFRESH_ON_RESET = """
322
+ function(button_value) {
323
+ if (button_value === "Reset") {
324
+ window.location.reload();
325
+ return false;
326
+ }
327
+ return true;
328
+ }
329
  """
 
330
 
 
 
 
331
 
332
+ # === MAIN APPLICATION ===
333
+ class WODAnalyzerApp:
334
+ """Main application class."""
335
+
336
+ def __init__(self):
337
+ self.ui_builder = UIBuilder()
338
+ self.event_handlers = EventHandlers()
339
+
340
+ def create_interface(self) -> gr.Blocks:
341
+ """Create the Gradio interface."""
342
+ with gr.Blocks(
343
+ theme=self.ui_builder.create_theme(),
344
+ js=JavaScriptFunctions.THEME_SETUP,
345
+ css=self.ui_builder.create_custom_css()
346
+ ) as demo:
347
+
348
+ # Header
349
+ gr.Markdown(f"{UIConstants.TITLE}\n{UIConstants.DESCRIPTION}")
350
+
351
+ # Input Section
352
+ with gr.Row():
353
+ file_input = gr.File(label="Upload WOD PDF")
354
+ type_input = gr.Dropdown(
355
+ choices=self.ui_builder.get_wod_type_options(),
356
+ label="Type",
357
+ value=UIConstants.DEFAULT_WOD_TYPE,
358
+ info="Select the type of work order."
359
+ )
360
+
361
+ # Action Button
362
+ analyze_btn = gr.Button(UIConstants.BUTTON_ANALYZE, variant="primary")
363
+
364
+ # Results Section
365
+ gr.Markdown("---\n## Results")
366
+
367
+ # Prediction display
368
+ prediction_output = gr.Markdown(value="", visible=True)
369
+
370
+ # JSON display for extracted data
371
+ with gr.Accordion("Extraction Result from Page 1", open=False, visible=False) as json_accordion:
372
+ json_output = gr.JSON(label="Extracted Data", show_label=False, open=True)
373
+
374
+ # Results table
375
+ results_output = gr.DataFrame(
376
+ headers=["Requirement", "Reason", "Status"],
377
+ datatype=["str", "str", "str"],
378
+ interactive=False,
379
+ max_height=UIConstants.TABLE_MAX_HEIGHT,
380
+ column_widths=UIConstants.COLUMN_WIDTHS,
381
+ wrap=True
382
+ )
383
+
384
+ # Event handling
385
+ analyze_btn.click(
386
+ fn=self.event_handlers.set_processing_state,
387
+ inputs=[analyze_btn],
388
+ outputs=[analyze_btn],
389
+ show_progress=False,
390
+ js=JavaScriptFunctions.REFRESH_ON_RESET
391
+ ).then(
392
+ fn=self.event_handlers.handle_analyze_button,
393
+ inputs=[file_input, type_input, analyze_btn],
394
+ outputs=[prediction_output, results_output, json_output, json_accordion, analyze_btn],
395
+ show_progress=True
396
+ )
397
+
398
+ return demo
399
+
400
+ def launch(self, enable_auth: bool = False) -> None:
401
+ """Launch the application."""
402
+ demo = self.create_interface()
403
+
404
+ if enable_auth:
405
+ demo.launch(
406
+ auth=AuthManager.authenticate_user,
407
+ auth_message="Please enter your credentials to access the WOD Analyzer",
408
+ debug=Config.DEBUG,
409
+ ssr_mode=False
410
+ )
411
+ else:
412
+ demo.launch(debug=Config.DEBUG)
413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
 
415
+ # === MAIN ENTRY POINT ===
416
+ def main():
417
+ """Main entry point."""
418
+ app = WODAnalyzerApp()
419
+
420
+ app.launch(enable_auth=PRODUCTION)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
 
422
 
423
+ if __name__ == "__main__":
424
+ main()