Skip to content

Commit 2286e71

Browse files
author
Irving Popovetsky
authored
Merge pull request #419 from OperationCode/irving/fix_issues_post_python_upgrade
fix high severity issues raised during code review
2 parents cfb227f + 87ad9f9 commit 2286e71

File tree

12 files changed

+594
-33
lines changed

12 files changed

+594
-33
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,7 @@ venv.bak/
115115
dmypy.json
116116

117117
# Pyre type checker
118-
.pyre/
118+
.pyre/
119+
120+
# Claude
121+
settings.local.json
File renamed without changes.

pybot/endpoints/airtable/utils.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ async def _get_requested_mentor(
1616
if not requested_mentor:
1717
return None
1818
mentor = await airtable.get_row_from_record_id("Mentors", requested_mentor)
19-
email = mentor["Email"]
19+
# mentor could be {} if the record wasn't found or an error occurred
20+
email = mentor.get("Email")
21+
if not email:
22+
return None
2023
slack_user_id = await _slack_user_id_from_email(email, slack)
2124
return f" Requested mentor: <@{slack_user_id}>"
2225
except SlackAPIError:
@@ -39,10 +42,15 @@ async def _get_matching_skillset_mentors(
3942
if not skillsets:
4043
return ["No skillset Given"]
4144
mentors = await airtable.find_mentors_with_matching_skillsets(skillsets)
42-
mentor_ids = [
43-
await _slack_user_id_from_email(mentor["Email"], slack, fallback=mentor["Slack Name"])
44-
for mentor in mentors
45-
]
45+
mentor_ids = []
46+
for mentor in mentors:
47+
email = mentor.get("Email")
48+
fallback = mentor.get("Slack Name", "Unknown Mentor")
49+
if email:
50+
mentor_id = await _slack_user_id_from_email(email, slack, fallback=fallback)
51+
mentor_ids.append(mentor_id)
52+
elif fallback:
53+
mentor_ids.append(fallback)
4654
return [f"<@{mentor}>" for mentor in mentor_ids]
4755

4856

pybot/endpoints/slack/actions/mentor_request.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,17 @@ async def mentor_request_submit(action: Action, app: SirBot):
2525

2626
username = action["user"]["name"]
2727
user_info = await slack.query(methods.USERS_INFO, {"user": action["user"]["id"]})
28-
email = user_info["user"]["profile"]["email"]
28+
email = user_info["user"]["profile"].get("email")
29+
30+
if not email:
31+
logger.warning(f"User {action['user']['id']} has no email in Slack profile")
32+
error_attachment = {
33+
"text": ":warning: Your Slack profile doesn't have an email address. Please update your profile and try again. :warning:",
34+
"color": "danger",
35+
}
36+
request.attachments = [error_attachment]
37+
await request.update_message(slack)
38+
return
2939

3040
airtable_response = await request.submit_request(username, email, airtable)
3141

@@ -127,7 +137,16 @@ async def claim_mentee(action: Action, app: SirBot):
127137
event = MentorRequestClaim(action, slack, airtable)
128138
if event.is_claim():
129139
user_info = await slack.query(methods.USERS_INFO, {"user": event.clicker})
130-
clicker_email = user_info["user"]["profile"]["email"]
140+
clicker_email = user_info["user"]["profile"].get("email")
141+
142+
if not clicker_email:
143+
logger.warning(f"Mentor {event.clicker} has no email in Slack profile")
144+
event.attachment["text"] = (
145+
f":warning: <@{event.clicker}>'s Slack profile has no email address. :warning:"
146+
)
147+
event.should_update = False
148+
await event.update_message()
149+
return
131150

132151
mentor_records = await airtable.find_records(
133152
table_name="Mentors", field="Email", value=clicker_email
@@ -140,5 +159,5 @@ async def claim_mentee(action: Action, app: SirBot):
140159

141160
await event.update_message()
142161

143-
except Exception as ex:
144-
logger.exception("Exception while updating claim", ex)
162+
except Exception:
163+
logger.exception("Exception while updating claim")

pybot/endpoints/slack/commands.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import logging
22
import random
33

4+
import aiohttp
5+
46
from pybot._vendor.sirbot import SirBot
57
from pybot._vendor.sirbot.plugins.slack import SlackPlugin
68
from pybot._vendor.slack import methods
@@ -91,12 +93,35 @@ async def slash_lunch(command: Command, app: SirBot):
9193

9294
slack = app["plugins"]["slack"].api
9395

94-
request = lunch.get_yelp_request()
95-
async with app.http_session.get(**request) as r:
96-
r.raise_for_status()
97-
message_params = lunch.select_random_lunch(await r.json())
98-
99-
await slack.query(methods.CHAT_POST_EPHEMERAL, message_params)
96+
try:
97+
request = lunch.get_yelp_request()
98+
async with app.http_session.get(**request) as r:
99+
r.raise_for_status()
100+
message_params = lunch.select_random_lunch(await r.json())
101+
except aiohttp.ClientResponseError as e:
102+
logger.error(f"Yelp API HTTP error for {command['user_name']}: {e.status} {e.message}")
103+
message_params = {
104+
"user": command["user_id"],
105+
"channel": command["channel_id"],
106+
"text": "Sorry, I couldn't reach the Yelp API right now. Please try again later.",
107+
}
108+
except aiohttp.ClientError as e:
109+
logger.error(f"Yelp API connection error for {command['user_name']}: {e}")
110+
message_params = {
111+
"user": command["user_id"],
112+
"channel": command["channel_id"],
113+
"text": "Sorry, I couldn't connect to Yelp. Please try again later.",
114+
}
115+
116+
if message_params is None:
117+
# No restaurants found in the area
118+
message_params = {
119+
"user": command["user_id"],
120+
"channel": command["channel_id"],
121+
"text": "Sorry, I couldn't find any restaurants in that area. Try a different zip code!",
122+
}
123+
124+
await slack.query(methods.CHAT_POST_EPHEMERAL, message_params)
100125

101126

102127
@catch_command_slack_error

pybot/endpoints/slack/message_templates/mentor_request.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def service(self, new_service):
3636
self.clear_errors()
3737

3838
@property
39-
def skillsets(self) -> [str]:
39+
def skillsets(self) -> list[str]:
4040
if self.skillset_fields:
4141
return [field["text"] for field in self.skillset_fields]
4242
return []
@@ -97,6 +97,13 @@ async def submit_request(self, username: str, email: str, airtable: AirtableAPI)
9797
params["Additional Details"] = self.details
9898

9999
service_records = await airtable.find_records("Services", "Name", self.service)
100+
if not service_records:
101+
return {
102+
"error": {
103+
"type": "SERVICE_NOT_FOUND",
104+
"message": f"Service '{self.service}' not found in Airtable",
105+
}
106+
}
100107
params["Service"] = [service_records[0]["id"]]
101108
return await airtable.add_record("Mentor Request", {"fields": params})
102109

pybot/endpoints/slack/utils/event_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,10 @@ async def link_backend_user(
8686
"""
8787

8888
user_info = await slack_api.query(methods.USERS_INFO, {"user": slack_id})
89-
email = user_info["user"]["profile"]["email"]
89+
email = user_info["user"]["profile"].get("email")
90+
if not email:
91+
logger.warning(f"User {slack_id} has no email in profile, skipping backend link")
92+
return
9093

9194
async with session.patch(
9295
f"{BACKEND_URL}/auth/profile/admin/",

pybot/endpoints/slack/utils/slash_lunch.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,20 @@ def get_yelp_request(self):
2828
"headers": self.AUTH_HEADER,
2929
}
3030

31-
def select_random_lunch(self, lunch_response: dict) -> dict:
32-
location_count = len(lunch_response["businesses"])
31+
def select_random_lunch(self, lunch_response: dict) -> dict | None:
32+
"""
33+
Select a random restaurant from the Yelp response.
34+
35+
Returns None if no businesses are found, allowing the caller to handle
36+
the empty result gracefully.
37+
"""
38+
businesses = lunch_response.get("businesses", [])
39+
if not businesses:
40+
logger.warning(f"No restaurants found for {self.user_name}")
41+
return None
3342

34-
selected_location = randint(0, location_count - 1)
35-
location = lunch_response["businesses"][selected_location]
43+
selected_location = randint(0, len(businesses) - 1)
44+
location = businesses[selected_location]
3645

3746
logger.info(f"location selected for {self.user_name}: {location}")
3847

pybot/plugins/airtable/api.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ async def _depaginate_records(self, url, params, offset):
3737
while offset:
3838
params["offset"] = offset
3939
response = await self.get(url, params=params)
40-
records.extend(response["records"])
40+
# Check for error response during pagination
41+
if "error" in response:
42+
error_msg = response["error"].get("message", "Unknown error")
43+
logger.error(f"Airtable API error during pagination: {error_msg}")
44+
break
45+
records.extend(response.get("records", []))
4146
offset = response.get("offset")
4247

4348
return records
@@ -50,7 +55,7 @@ def table_url(self, table_name, record_id=None):
5055

5156
async def get_name_from_record_id(self, table_name: str, record_id):
5257
if self.record_id_to_name[table_name]:
53-
return self.record_id_to_name[table_name][record_id]
58+
return self.record_id_to_name[table_name].get(record_id)
5459

5560
url = self.table_url("Services")
5661
params = {"fields[]": "Name"}
@@ -63,10 +68,13 @@ async def get_name_from_record_id(self, table_name: str, record_id):
6368
return None
6469

6570
records = res_json["records"]
71+
# Skip records that don't have a Name field (Airtable omits empty fields)
6672
self.record_id_to_name[table_name] = {
67-
record["id"]: record["fields"]["Name"] for record in records
73+
record["id"]: record["fields"]["Name"]
74+
for record in records
75+
if "Name" in record.get("fields", {})
6876
}
69-
return self.record_id_to_name[table_name][record_id]
77+
return self.record_id_to_name[table_name].get(record_id)
7078

7179
async def get_row_from_record_id(self, table_name: str, record_id: str) -> dict:
7280
url = self.table_url(table_name, record_id)
@@ -80,8 +88,8 @@ async def get_row_from_record_id(self, table_name: str, record_id: str) -> dict:
8088
return {}
8189

8290
return res_json["fields"]
83-
except Exception as ex:
84-
logger.exception(f"Couldn't get row from record id {record_id} in {table_name}", ex)
91+
except Exception:
92+
logger.exception(f"Couldn't get row from record id {record_id} in {table_name}")
8593
return {}
8694

8795
async def get_all_records(self, table_name, field=None):
@@ -106,7 +114,12 @@ async def get_all_records(self, table_name, field=None):
106114
)
107115

108116
if field:
109-
return [record["fields"][field] for record in res_json["records"]]
117+
# Skip records that don't have the field (Airtable omits empty fields)
118+
return [
119+
record["fields"][field]
120+
for record in res_json["records"]
121+
if field in record.get("fields", {})
122+
]
110123
else:
111124
return res_json["records"]
112125

@@ -141,8 +154,8 @@ async def find_mentors_with_matching_skillsets(self, skillsets):
141154
for skillset in skillsets
142155
):
143156
partial_match.append(mentor["fields"])
144-
except Exception as e:
145-
logger.exception("Exception while trying to find filter mentors by skillset", e)
157+
except Exception:
158+
logger.exception("Exception while trying to filter mentors by skillset")
146159
return []
147160

148161
if len(complete_match) < 5:
@@ -165,8 +178,8 @@ async def find_records(self, table_name: str, field: str, value: str) -> list:
165178
return []
166179

167180
return response["records"]
168-
except Exception as ex:
169-
logger.exception(f"Exception when attempting to get {field} from {table_name}.", ex)
181+
except Exception:
182+
logger.exception(f"Exception when attempting to get {field} from {table_name}")
170183
return []
171184

172185
async def update_request(self, request_record, mentor_id):

tests/integration/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Integration tests for pybot

0 commit comments

Comments
 (0)