Skip to content

Commit 33eca1b

Browse files
Source Code cleanups and improvements based on feedback from https://news.ycombinator.com/user?id=necovek
The initial Cursor/claude-3.7-sonnet(thinking) prompt was: I have received the following feedback regarding this codebase: "The premise might possibly be true, but as an actually seasoned Python developer, I've taken a look at one file: @utils.py. All of it smells of a (lousy) junior software engineer: from configuring root logger at the top, module level (which relies on module import caching not to be reapplied), over not using a stdlib config file parser and building one themselves, to a raciness in load_json where it's checked for file existence with an if and then carrying on as if the file is certainly there..." I therefore ask you to thoroughly improve the code quality of the implementation in @src while staying in line with the requirements from @REQUIREMENTS.md, and while ensuring that the Quality Tools (see @makefile) won't fail. Also, make sure that the tests in folder @tests don't break. See file @pyproject.toml for the general project setup. There is already a virtualenv at @venv. -- This was followed by another prompt: Check the other files in @src and identify where improvements are needed. Then do the improvements while ensuring Code Quality with the available quality tools and tests. -- This was followed by another prompt: Now improve ALL files @step1_prepare.py @step2_download_previous_state.py @step3_retrieve_hourly_problem_numbers.py @step4_generate_trend_chart.py @step5_download_logstash_documents.py @step6_extract_fields.py @step7_normalize_messages.py @step8_compare_normalizations.py @step9_generate_email_bodies.py @step10_send_email_report.py @step11_store_new_state.py @step12_cleanup.py using the same process. -- A partial screen recording of the session is available at https://www.youtube.com/watch?v=zUSm1_NFKpA
1 parent a55b12e commit 33eca1b

11 files changed

+796
-318
lines changed

src/platform_problem_monitoring_core/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@
33
A tool for monitoring platform problems using Elasticsearch logs.
44
"""
55

6-
__version__ = "0.1.0"
6+
from importlib.metadata import PackageNotFoundError, version
7+
8+
try:
9+
__version__ = version("platform_problem_monitoring_core")
10+
except PackageNotFoundError:
11+
__version__ = "0.1.0" # Default version if package is not installed

src/platform_problem_monitoring_core/step10_send_email_report.py

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import argparse
55
import smtplib
6+
import ssl
67
import sys
78
from email.mime.multipart import MIMEMultipart
89
from email.mime.text import MIMEText
@@ -74,6 +75,7 @@ def send_email_report(
7475
smtp_pass: str,
7576
sender: str,
7677
receiver: str,
78+
use_tls: bool = True,
7779
) -> None:
7880
"""
7981
Send email report.
@@ -88,6 +90,12 @@ def send_email_report(
8890
smtp_pass: SMTP password
8991
sender: Sender email address
9092
receiver: Receiver email address
93+
use_tls: Whether to use TLS encryption (default: True)
94+
95+
Raises:
96+
FileNotFoundError: If either email body file doesn't exist
97+
smtplib.SMTPException: If there's an error sending the email
98+
OSError: If there's an error reading the email body files
9199
"""
92100
logger.info("Sending email report")
93101
logger.info(f"HTML file: {html_file}")
@@ -98,13 +106,28 @@ def send_email_report(
98106
logger.info(f"SMTP user: {smtp_user}")
99107
logger.info(f"Sender: {sender}")
100108
logger.info(f"Receiver: {receiver}")
109+
logger.info(f"Use TLS: {use_tls}")
110+
111+
# Check if files exist
112+
html_path = Path(html_file)
113+
text_path = Path(text_file)
114+
115+
if not html_path.exists():
116+
error_msg = f"HTML email body file not found: {html_file}"
117+
logger.error(error_msg)
118+
raise FileNotFoundError(error_msg)
119+
120+
if not text_path.exists():
121+
error_msg = f"Text email body file not found: {text_file}"
122+
logger.error(error_msg)
123+
raise FileNotFoundError(error_msg)
101124

102125
try:
103126
# Read the email bodies
104-
with Path(html_file).open("r") as f:
127+
with html_path.open("r") as f:
105128
html_body = f.read()
106129

107-
with Path(text_file).open("r") as f:
130+
with text_path.open("r") as f:
108131
text_body = f.read()
109132

110133
# Wrap long lines to avoid SMTP line length limits (RFC 5322 says 998 characters max)
@@ -125,21 +148,37 @@ def send_email_report(
125148
logger.info(f"Connecting to SMTP server {smtp_host}:{smtp_port}")
126149

127150
# Send the email
128-
server = smtplib.SMTP(smtp_host, smtp_port)
129-
server.starttls()
130-
server.login(smtp_user, smtp_pass)
131-
server.sendmail(sender, receiver, msg.as_string())
132-
server.quit()
133-
134-
logger.info("Email report sent successfully")
135-
except FileNotFoundError as e:
136-
logger.error(f"Email body file not found: {str(e)}")
137-
raise
138-
except smtplib.SMTPException as e:
139-
logger.error(f"SMTP error: {str(e)}")
140-
raise
141-
except Exception as e:
142-
logger.error(f"Unexpected error sending email: {str(e)}")
151+
server = None
152+
try:
153+
server = smtplib.SMTP(smtp_host, smtp_port, timeout=30)
154+
155+
# Optional: Enable debug output
156+
# server.set_debuglevel(1)
157+
158+
# Use TLS if requested
159+
if use_tls:
160+
context = ssl.create_default_context()
161+
server.starttls(context=context)
162+
163+
server.login(smtp_user, smtp_pass)
164+
server.sendmail(sender, receiver, msg.as_string())
165+
logger.info("Email report sent successfully")
166+
except smtplib.SMTPException as e:
167+
error_msg = f"SMTP error: {str(e)}"
168+
logger.error(error_msg)
169+
raise
170+
finally:
171+
if server is not None:
172+
server.quit()
173+
logger.debug("SMTP connection closed")
174+
175+
except (OSError, smtplib.SMTPException) as e:
176+
if isinstance(e, FileNotFoundError):
177+
logger.error(f"Email body file not found: {e}")
178+
elif isinstance(e, smtplib.SMTPException):
179+
logger.error(f"SMTP error: {e}")
180+
else:
181+
logger.error(f"Error sending email: {e}")
143182
raise
144183

145184

@@ -155,6 +194,7 @@ def main() -> None:
155194
parser.add_argument("--smtp-pass", required=True, help="SMTP password")
156195
parser.add_argument("--sender", required=True, help="Sender email address")
157196
parser.add_argument("--receiver", required=True, help="Receiver email address")
197+
parser.add_argument("--no-tls", action="store_true", help="Disable TLS encryption")
158198

159199
args = parser.parse_args()
160200

@@ -169,6 +209,7 @@ def main() -> None:
169209
args.smtp_pass,
170210
args.sender,
171211
args.receiver,
212+
not args.no_tls,
172213
)
173214
sys.exit(0)
174215
except Exception as e:

src/platform_problem_monitoring_core/step11_store_new_state.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pathlib import Path
77

88
import boto3
9-
from botocore.exceptions import ClientError
9+
from botocore.exceptions import ClientError, NoCredentialsError
1010

1111
from platform_problem_monitoring_core.utils import logger
1212

@@ -20,6 +20,11 @@ def store_new_state(s3_bucket: str, s3_folder: str, date_time_file: str, norm_re
2020
s3_folder: S3 folder name
2121
date_time_file: Path to the date and time file to upload
2222
norm_results_file: Path to the normalization results file to upload
23+
24+
Raises:
25+
FileNotFoundError: If either input file doesn't exist
26+
NoCredentialsError: If AWS credentials are not found
27+
ClientError: If any AWS S3 operation fails
2328
"""
2429
logger.info("Storing new state")
2530
logger.info(f"S3 bucket: {s3_bucket}")
@@ -28,37 +33,62 @@ def store_new_state(s3_bucket: str, s3_folder: str, date_time_file: str, norm_re
2833
logger.info(f"Normalization results file: {norm_results_file}")
2934

3035
# Check if files exist
31-
if not Path(date_time_file).exists():
36+
date_time_path = Path(date_time_file)
37+
norm_results_path = Path(norm_results_file)
38+
39+
if not date_time_path.exists():
3240
error_msg = f"Date time file not found: {date_time_file}"
3341
logger.error(error_msg)
3442
raise FileNotFoundError(error_msg)
3543

36-
if not Path(norm_results_file).exists():
44+
if not norm_results_path.exists():
3745
error_msg = f"Normalization results file not found: {norm_results_file}"
3846
logger.error(error_msg)
3947
raise FileNotFoundError(error_msg)
4048

41-
# Create S3 client
42-
s3_client = boto3.client("s3")
43-
44-
# Upload date time file
45-
date_time_key = f"{s3_folder}/current_date_time.txt"
4649
try:
47-
logger.info(f"Uploading date time file to s3://{s3_bucket}/{date_time_key}")
48-
s3_client.upload_file(date_time_file, s3_bucket, date_time_key)
49-
logger.info("Date time file uploaded successfully")
50-
except ClientError as e:
51-
logger.error(f"Failed to upload date time file: {e}")
50+
# Create S3 client
51+
s3_client = boto3.client("s3")
52+
53+
# Test connection to S3 by checking bucket existence
54+
s3_client.head_bucket(Bucket=s3_bucket)
55+
logger.info(f"Successfully connected to S3 bucket: {s3_bucket}")
56+
57+
# Upload date time file
58+
date_time_key = f"{s3_folder}/current_date_time.txt"
59+
try:
60+
logger.info(f"Uploading date time file to s3://{s3_bucket}/{date_time_key}")
61+
s3_client.upload_file(date_time_file, s3_bucket, date_time_key)
62+
logger.info("Date time file uploaded successfully")
63+
except ClientError as e:
64+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
65+
error_msg = f"Failed to upload date time file: Error {error_code} - {str(e)}"
66+
logger.error(error_msg)
67+
raise ClientError(e.response, e.operation_name) from e
68+
69+
# Upload normalization results file
70+
norm_results_key = f"{s3_folder}/norm_results.json"
71+
try:
72+
logger.info(f"Uploading normalization results to s3://{s3_bucket}/{norm_results_key}")
73+
s3_client.upload_file(norm_results_file, s3_bucket, norm_results_key)
74+
logger.info("Normalization results uploaded successfully")
75+
except ClientError as e:
76+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
77+
error_msg = f"Failed to upload normalization results: Error {error_code} - {str(e)}"
78+
logger.error(error_msg)
79+
raise ClientError(e.response, e.operation_name) from e
80+
81+
except NoCredentialsError as e:
82+
logger.error(f"AWS credentials not found: {e}")
5283
raise
53-
54-
# Upload normalization results file
55-
norm_results_key = f"{s3_folder}/norm_results.json"
56-
try:
57-
logger.info(f"Uploading normalization results to s3://{s3_bucket}/{norm_results_key}")
58-
s3_client.upload_file(norm_results_file, s3_bucket, norm_results_key)
59-
logger.info("Normalization results uploaded successfully")
6084
except ClientError as e:
61-
logger.error(f"Failed to upload normalization results: {e}")
85+
if e.response.get("Error", {}).get("Code") == "NoSuchBucket":
86+
logger.error(f"S3 bucket not found: {s3_bucket}")
87+
else:
88+
logger.error(f"AWS S3 error: {e}")
89+
raise
90+
except Exception as e:
91+
logger.error(f"Unexpected error storing state: {e}")
6292
raise
6393

6494
logger.info("New state stored successfully")
@@ -80,6 +110,7 @@ def main() -> None:
80110

81111
try:
82112
store_new_state(args.s3_bucket, args.s3_folder, args.date_time_file, args.norm_results_file)
113+
sys.exit(0)
83114
except Exception as e:
84115
logger.error(f"Error storing new state: {e}")
85116
sys.exit(1)

src/platform_problem_monitoring_core/step12_cleanup.py

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,103 @@
22
"""Clean up work environment."""
33

44
import argparse
5+
import logging
6+
import os
57
import shutil
68
import sys
79
from pathlib import Path
10+
from typing import List
811

912
from platform_problem_monitoring_core.utils import logger
1013

1114

12-
def cleanup_environment(work_dir: str) -> None:
15+
def _verify_safe_path(work_dir: Path) -> None:
1316
"""
14-
Clean up the work environment by removing the temporary work directory.
17+
Verify that the path is safe to remove.
1518
1619
Args:
17-
work_dir: Path to the temporary work folder to remove
18-
"""
19-
logger.info("Cleaning up work environment")
20-
logger.info(f"Removing temporary work directory: {work_dir}")
21-
22-
# Convert to Path object
23-
work_path = Path(work_dir)
20+
work_dir: Path to verify
2421
22+
Raises:
23+
ValueError: If the path is not a directory, doesn't exist, or doesn't look like a temporary work directory
24+
"""
2525
# Check if the directory exists
26-
if not work_path.exists():
27-
logger.warning(f"Work directory does not exist: {work_dir}")
28-
return
26+
if not work_dir.exists():
27+
error_msg = f"Directory does not exist: {work_dir}"
28+
raise ValueError(error_msg)
2929

3030
# Check if the path is a directory
31-
if not work_path.is_dir():
31+
if not work_dir.is_dir():
3232
error_msg = f"Path is not a directory: {work_dir}"
33-
logger.error(error_msg)
3433
raise ValueError(error_msg)
3534

3635
# Verify that the directory looks like a temporary work directory
3736
# This is a safety check to avoid accidentally deleting important directories
38-
if not work_path.name.startswith("platform_problem_monitoring_"):
37+
if not work_dir.name.startswith("platform_problem_monitoring_"):
3938
error_msg = f"Directory does not appear to be a temporary work directory: {work_dir}"
40-
logger.error(error_msg)
4139
raise ValueError(error_msg)
4240

41+
42+
def _list_remaining_files(work_dir: Path) -> List[str]:
43+
"""
44+
List files remaining in the directory.
45+
46+
Args:
47+
work_dir: Path to the directory
48+
49+
Returns:
50+
List of files found in the directory
51+
"""
52+
files = []
4353
try:
54+
for root, _dirs, filenames in os.walk(work_dir):
55+
for filename in filenames:
56+
file_path = Path(root) / filename
57+
files.append(str(file_path.relative_to(work_dir)))
58+
return files
59+
except (OSError, ValueError) as e:
60+
logger.warning(f"Error listing files in {work_dir}: {e}")
61+
return []
62+
63+
64+
def cleanup_environment(work_dir: str) -> None:
65+
"""
66+
Clean up the work environment by removing the temporary work directory.
67+
68+
Args:
69+
work_dir: Path to the temporary work folder to remove
70+
71+
Raises:
72+
ValueError: If the path is not suitable for removal
73+
OSError: If there's an error removing the directory
74+
"""
75+
logger.info("Cleaning up work environment")
76+
logger.info(f"Removing temporary work directory: {work_dir}")
77+
78+
# Convert to Path object
79+
work_path = Path(work_dir)
80+
81+
try:
82+
# Check if the path is safe to remove
83+
_verify_safe_path(work_path)
84+
85+
# Optional: List files before deletion (for debugging if needed)
86+
if logger.isEnabledFor(logging.DEBUG):
87+
files = _list_remaining_files(work_path)
88+
if files:
89+
logger.debug(f"Files to be removed: {', '.join(files)}")
90+
4491
# Remove the directory and all its contents
4592
shutil.rmtree(work_dir)
4693
logger.info(f"Successfully removed directory: {work_dir}")
47-
except Exception as e:
48-
logger.error(f"Error removing directory {work_dir}: {str(e)}")
49-
raise
94+
95+
except ValueError as e:
96+
# Non-fatal errors (directory doesn't exist or isn't a temp directory)
97+
logger.warning(f"Skipping cleanup: {str(e)}")
98+
except OSError as e:
99+
error_msg = f"Error removing directory {work_dir}: {str(e)}"
100+
logger.error(error_msg)
101+
raise OSError(error_msg) from e
50102

51103
logger.info("Cleanup complete")
52104

0 commit comments

Comments
 (0)