|
28 | 28 | # from src.analysis.frequency_analysis import FrequencyAnalysis # noqa: E402 |
29 | 29 | from src.config.enum_constants import EfficiencyCategoryEnum # noqa: E402 |
30 | 30 | from src.analysis.user_comparison import UserComparison # noqa: E402 |
| 31 | +from src.analysis.user import get_metrics as get_user_metrics # noqa: E402 |
31 | 32 | from src.utilities.report_generation import ( # noqa: E402 |
32 | 33 | calculate_analysis_period, |
33 | 34 | calculate_summary_statistics, |
34 | 35 | calculate_comparison_statistics, |
35 | 36 | generate_recommendations, |
36 | | - get_total_job_count, |
37 | 37 | ) |
38 | 38 |
|
39 | 39 |
|
@@ -101,20 +101,11 @@ def generate_user_report( |
101 | 101 | # Calculate analysis period using shared utility |
102 | 102 | analysis_period = calculate_analysis_period(user_jobs) |
103 | 103 |
|
104 | | - # === CALCULATE SUMMARY STATISTICS USING SHARED UTILITIES === |
105 | | - # Use the get_total_job_count function if db connection is provided |
106 | | - if db: |
107 | | - total_all_jobs = get_total_job_count(db, analysis_period) |
108 | | - else: |
109 | | - total_all_jobs = len(job_data) # Fallback to current dataset size |
110 | | - |
111 | 104 | # Use shared utility for summary statistics |
112 | 105 | summary_stats = calculate_summary_statistics( |
113 | 106 | user_jobs=user_jobs, |
114 | | - all_jobs_count=total_all_jobs, |
115 | 107 | analysis_period=analysis_period |
116 | 108 | ) |
117 | | - |
118 | 109 | # === CALCULATE COMPARISON STATISTICS === |
119 | 110 | # Use shared utility for comparison statistics |
120 | 111 | if user_comparison and user_comparison._cached_all_users_metrics is not None: |
@@ -271,6 +262,131 @@ def check_directory_writable(directory_path: str) -> tuple[bool, str | None]: |
271 | 262 | return False, str(e) |
272 | 263 |
|
273 | 264 |
|
| 265 | +def generate_single_report( |
| 266 | + user_id: str, |
| 267 | + db_path: str = "./slurm_data.db", |
| 268 | + output_dir: str = "./reports/user_reports", |
| 269 | + template_path: str = "./reports/user_no_compare_report_template.qmd", |
| 270 | + output_format: str = "html", |
| 271 | +) -> None: |
| 272 | + """Generate a report for a single user without comparison.""" |
| 273 | + |
| 274 | + user_jobs, user_metrics = get_user_metrics(db_path, user_id) |
| 275 | + |
| 276 | + if not len(user_jobs): |
| 277 | + print("No jobs available.") |
| 278 | + return |
| 279 | + |
| 280 | + user_output_dir = os.path.join(output_dir, f"user_{user_id}") |
| 281 | + os.makedirs(user_output_dir, exist_ok=True) |
| 282 | + |
| 283 | + output_file = f"{user_id}_report.{output_format}" |
| 284 | + final_output_path = os.path.join(user_output_dir, output_file) |
| 285 | + |
| 286 | + # Check that the template exists |
| 287 | + if not os.path.exists(template_path): |
| 288 | + abs_template_path = os.path.abspath(template_path) |
| 289 | + if os.path.exists(abs_template_path): |
| 290 | + template_path = abs_template_path |
| 291 | + else: |
| 292 | + print(f"Error: Template file not found at: {template_path}") |
| 293 | + print(f"Absolute path also not found: {abs_template_path}") |
| 294 | + return None |
| 295 | + |
| 296 | + # Calculate dates for the report |
| 297 | + start_date = user_jobs["StartTime"].min().strftime("%Y-%m-%d") |
| 298 | + end_date = user_jobs["StartTime"].max().strftime("%Y-%m-%d") |
| 299 | + |
| 300 | + # Calculate analysis period using shared utility |
| 301 | + analysis_period = calculate_analysis_period(user_jobs) |
| 302 | + |
| 303 | + # Use shared utility for summary statistics |
| 304 | + summary_stats = calculate_summary_statistics( |
| 305 | + user_jobs=user_jobs, |
| 306 | + analysis_period=analysis_period |
| 307 | + ) |
| 308 | + print(summary_stats) |
| 309 | + |
| 310 | + # === PREPARE GPU TYPE DATA === |
| 311 | + if "GPUType" in user_jobs.columns and "primary_gpu_type" in user_jobs.columns: |
| 312 | + gpu_type_data = user_jobs["primary_gpu_type"].value_counts().reset_index() |
| 313 | + gpu_type_data.columns = ["gpu_type", "job_count"] |
| 314 | + else: |
| 315 | + gpu_type_data = pd.DataFrame(columns=["gpu_type", "job_count"]) |
| 316 | + |
| 317 | + # Generate recommendations using shared utility |
| 318 | + recommendations = generate_recommendations(user_jobs, user_metrics) |
| 319 | + |
| 320 | + # === SAVE DATA TO JSON FOR QUARTO === |
| 321 | + |
| 322 | + temp_data_pkl = { |
| 323 | + "user_id": user_id, |
| 324 | + "start_date": start_date, |
| 325 | + "end_date": end_date, |
| 326 | + "analysis_period": analysis_period, |
| 327 | + "summary_stats": summary_stats, |
| 328 | + "gpu_type_data": gpu_type_data, |
| 329 | + "recommendations": recommendations, |
| 330 | + "user_data": pd.DataFrame(user_metrics), |
| 331 | + "user_jobs": pd.DataFrame(user_jobs), |
| 332 | + } |
| 333 | + |
| 334 | + # get abs path to the template in the reports folder |
| 335 | + template_file = os.path.join(project_root, template_path) |
| 336 | + |
| 337 | + with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as f: |
| 338 | + pickle.dump(temp_data_pkl, f, protocol=pickle.HIGHEST_PROTOCOL) |
| 339 | + temp_pickle_path = f.name |
| 340 | + |
| 341 | + permanent_path = os.path.join(project_root, "reports", "test_data.pkl") |
| 342 | + shutil.copy(temp_pickle_path, permanent_path) |
| 343 | + |
| 344 | + # Use absolute path for output file to avoid path resolution issues |
| 345 | + output_file_abs = os.path.abspath(final_output_path) |
| 346 | + |
| 347 | + cmd = ["quarto", "render", template_file, "--to", output_format, "-o", output_file, "--execute"] |
| 348 | + cmd.extend(["-P", f"pickle_file:{temp_pickle_path}"]) |
| 349 | + try: |
| 350 | + # Run Quarto in the reports directory (full path) |
| 351 | + reports_dir = os.path.join(project_root, "reports") |
| 352 | + print("Starting Quarto rendering...") |
| 353 | + print(f"Output file: {output_file_abs}") |
| 354 | + print(f"Command: {' '.join(cmd)}") |
| 355 | + |
| 356 | + result = subprocess.run( |
| 357 | + cmd, |
| 358 | + check=False, # Don't raise exception, handle it manually |
| 359 | + capture_output=True, |
| 360 | + text=True, |
| 361 | + cwd=reports_dir, |
| 362 | + ) |
| 363 | + output_path = os.path.join(reports_dir, output_file) |
| 364 | + # Check if command was successful |
| 365 | + if result.returncode != 0: |
| 366 | + print(f" ❌ Quarto rendering failed (code {result.returncode})") |
| 367 | + if result.stderr: |
| 368 | + print(f" Error: {result.stderr.strip()}") |
| 369 | + if result.stdout: |
| 370 | + print(f" Output: {result.stdout.strip()}") |
| 371 | + return None |
| 372 | + |
| 373 | + # Check if output file was created |
| 374 | + if not os.path.exists(output_path): |
| 375 | + print(" ❌ Output file not created") |
| 376 | + print(f" Expected at: {output_file_abs}") |
| 377 | + return None |
| 378 | + |
| 379 | + print(f" ✅ Report saved: {os.path.basename(final_output_path)}") |
| 380 | + return final_output_path |
| 381 | + |
| 382 | + except subprocess.CalledProcessError as e: |
| 383 | + print(f" ❌ Process error: {e}") |
| 384 | + return None |
| 385 | + except Exception as e: |
| 386 | + print(f" ❌ Unexpected error: {e}") |
| 387 | + return None |
| 388 | + |
| 389 | + |
274 | 390 | def generate_reports_for_specific_users( |
275 | 391 | user_list: list, |
276 | 392 | db_path: str = "./slurm_data.db", |
@@ -485,6 +601,7 @@ def generate_all_reports( |
485 | 601 | users_parser.add_argument( |
486 | 602 | "--min-jobs", type=int, default=10, help="Minimum number of jobs a user must have (default: 10)" |
487 | 603 | ) |
| 604 | + users_parser.add_argument("--no-compare", action="store_true", default=False) |
488 | 605 |
|
489 | 606 | # Common arguments for both modes |
490 | 607 | parser.add_argument( |
@@ -541,14 +658,18 @@ def generate_all_reports( |
541 | 658 | print("👥 GENERATING REPORTS FOR SPECIFIC USERS") |
542 | 659 | print(f" Users: {', '.join(user_list)}") |
543 | 660 | print(f" Minimum jobs required: {args.min_jobs}") |
544 | | - report_paths = generate_reports_for_specific_users( |
545 | | - user_list=user_list, |
546 | | - db_path=args.db_path, |
547 | | - output_dir=args.output_dir, |
548 | | - template_path=args.template, |
549 | | - min_jobs=args.min_jobs, |
550 | | - output_format=args.format, |
551 | | - ) |
| 661 | + if args.no_compare: |
| 662 | + for user in user_list: |
| 663 | + generate_single_report(user, args.db_path) |
| 664 | + else: |
| 665 | + report_paths = generate_reports_for_specific_users( |
| 666 | + user_list=user_list, |
| 667 | + db_path=args.db_path, |
| 668 | + output_dir=args.output_dir, |
| 669 | + template_path=args.template, |
| 670 | + min_jobs=args.min_jobs, |
| 671 | + output_format=args.format, |
| 672 | + ) |
552 | 673 |
|
553 | 674 | print("\n🎉 ALL TASKS COMPLETED!") |
554 | 675 | print(f" Output directory: {args.output_dir}") |
|
0 commit comments