11import os
22import json
33import logging
4+ import csv
5+ import smtplib
6+ from email .mime .text import MIMEText
7+ from email .mime .multipart import MIMEMultipart
8+ from email .mime .base import MIMEBase
9+ from email import encoders
410from typing import Dict , Any , List , Optional , Tuple
11+ from datetime import datetime
512from sqlalchemy import create_engine , text
613from sqlalchemy .engine import Connection
714import requests
@@ -62,21 +69,6 @@ def fetch_data(conn: Connection, sql_limit: int, eval_function_name: str, grade_
6269 # Combine clauses with AND
6370 where_sql = " AND " .join (where_clauses )
6471
65- # Start with mandatory filters
66- where_clauses = ["EF.name = :name_param" ]
67- params = {
68- "name_param" : eval_function_name ,
69- "limit_param" : limit
70- }
71-
72- # Conditionally add the gradeParams filter
73- if grade_params_json :
74- where_clauses .append ("RA.\" gradeParams\" ::jsonb = (:params_param)::jsonb" )
75- params ["params_param" ] = grade_params_json
76-
77- # Combine clauses with AND
78- where_sql = " AND " .join (where_clauses )
79-
8072 sql_query_template = f"""
8173 SELECT
8274 S.submission, S.answer, S.grade, RA."gradeParams"::json as grade_params
@@ -139,7 +131,6 @@ def _execute_request(endpoint_path: str, payload: Dict[str, Any]) -> Tuple[
139131 timeout = 10 ,
140132 )
141133
142-
143134 if response .status_code != 200 :
144135 return None , {
145136 "error_type" : "HTTP Error" ,
@@ -242,6 +233,114 @@ def test_endpoint(base_endpoint: str, data_records: List[Dict[str, Any]]) -> Dic
242233 }
243234
244235
236+ def write_errors_to_csv (errors : List [Dict [str , Any ]], filename : str ) -> Optional [str ]:
237+ """Write error list to CSV file."""
238+ if not errors :
239+ logger .info ("No errors to write to CSV." )
240+ return None
241+
242+ try :
243+ filepath = f"/tmp/{ filename } "
244+
245+ # Get all unique keys from all error dictionaries
246+ fieldnames = set ()
247+ for error in errors :
248+ fieldnames .update (error .keys ())
249+ fieldnames = sorted (list (fieldnames ))
250+
251+ with open (filepath , 'w' , newline = '' , encoding = 'utf-8' ) as f :
252+ writer = csv .DictWriter (f , fieldnames = fieldnames )
253+ writer .writeheader ()
254+ writer .writerows (errors )
255+
256+ logger .info (f"CSV file created: { filepath } " )
257+ return filepath
258+
259+ except Exception as e :
260+ logger .error (f"Failed to create CSV: { e } " )
261+ return None
262+
263+
264+ def send_email_with_results (results : Dict [str , Any ], csv_path : Optional [str ],
265+ endpoint : str , eval_function_name : str , recipient_email : str ):
266+ """Send email with test results and CSV attachment."""
267+
268+ # Get email config from environment variables
269+ sender_email = os .environ .get ('SENDER_EMAIL' )
270+ sender_password = os .environ .get ('SENDER_PASSWORD' )
271+
272+ if not all ([sender_email , sender_password , recipient_email ]):
273+ logger .warning ("Email credentials not configured. Skipping email notification." )
274+ return
275+
276+ try :
277+ # Calculate pass rate
278+ pass_count = results ['pass_count' ]
279+ total_count = results ['total_count' ]
280+ pass_rate = (pass_count / total_count * 100 ) if total_count > 0 else 0
281+
282+ # Determine status
283+ status = "✓ PASSED" if results ['number_of_errors' ] == 0 else "✗ FAILED"
284+
285+ # Create email
286+ msg = MIMEMultipart ()
287+ msg ['From' ] = sender_email
288+ msg ['To' ] = recipient_email
289+ msg ['Subject' ] = f"Endpoint Test Results - { status } - { eval_function_name } "
290+
291+ # Email body
292+ body = f"""
293+ Evaluation Function Testing Report
294+ =======================
295+
296+ Test Completed: { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )}
297+ Endpoint: { endpoint }
298+ Evaluation Function: { eval_function_name }
299+
300+ Results Summary:
301+ ----------------
302+ Status: { status }
303+ Pass Rate: { pass_rate :.1f} % ({ pass_count } /{ total_count } )
304+ Total Tests: { total_count }
305+ Passed: { pass_count }
306+ Failed: { results ['number_of_errors' ]}
307+
308+ { f"⚠ Warning: Testing stopped early after reaching { MAX_ERROR_THRESHOLD } errors." if results ['number_of_errors' ] >= MAX_ERROR_THRESHOLD else "" }
309+
310+ { 'Detailed error information is attached in the CSV file.' if csv_path else 'No errors encountered - all tests passed!' }
311+
312+ This is an automated notification from the endpoint testing system.
313+ """
314+
315+ msg .attach (MIMEText (body , 'plain' ))
316+
317+ # Attach CSV if it exists
318+ if csv_path and os .path .exists (csv_path ):
319+ with open (csv_path , 'rb' ) as f :
320+ part = MIMEBase ('application' , 'octet-stream' )
321+ part .set_payload (f .read ())
322+
323+ encoders .encode_base64 (part )
324+ part .add_header (
325+ 'Content-Disposition' ,
326+ f'attachment; filename={ os .path .basename (csv_path )} '
327+ )
328+ msg .attach (part )
329+ logger .info (f"Attached CSV file: { csv_path } " )
330+
331+ # Send email (Gmail SMTP)
332+ server = smtplib .SMTP ('smtp.gmail.com' , 587 )
333+ server .starttls ()
334+ server .login (sender_email , sender_password )
335+ server .send_message (msg )
336+ server .quit ()
337+
338+ logger .info (f"Email sent successfully to { recipient_email } " )
339+
340+ except Exception as e :
341+ logger .error (f"Failed to send email: { e } " )
342+
343+
245344def lambda_handler (event : Dict [str , Any ], context : Any ) -> Dict [str , Any ]:
246345 """Main Lambda function entry point."""
247346 conn = None
@@ -256,6 +355,7 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
256355
257356 eval_function_name = payload .get ('eval_function_name' )
258357 grade_params_json = payload .get ('grade_params_json' )
358+ recipient_email = payload .get ('recipient_email' )
259359
260360 if not endpoint_to_test or not eval_function_name :
261361 missing_fields = []
@@ -269,6 +369,16 @@ def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
269369
270370 results = test_endpoint (endpoint_to_test , data_for_test )
271371
372+ # Write errors to CSV and send email
373+ csv_path = None
374+ if results ['list_of_errors' ]:
375+ timestamp = datetime .now ().strftime ('%Y%m%d_%H%M%S' )
376+ csv_filename = f"endpoint_test_errors_{ eval_function_name } _{ timestamp } .csv"
377+ csv_path = write_errors_to_csv (results ['list_of_errors' ], csv_filename )
378+
379+ # Send email notification with results
380+ send_email_with_results (results , csv_path , endpoint_to_test , eval_function_name )
381+
272382 return {
273383 "statusCode" : 200 ,
274384 "body" : json .dumps ({
0 commit comments