Compare commits

..

No commits in common. "de11f8059d70d1aab7c7ff240df9a6f388db911b" and "0e5992c0f47af7d57bf8ab412b6d0c64c3e78943" have entirely different histories.

3 changed files with 55 additions and 150 deletions

View file

@ -1,6 +0,0 @@
name: Dependency Review
on:
pull_request:
jobs:
review:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@main

View file

@ -53,7 +53,6 @@ def _get_slack_token():
# Front API
# ---------------------------------------------------------------------------
def _front_get(url_or_path, params=None):
if url_or_path.startswith("http"):
url = url_or_path
@ -62,13 +61,10 @@ def _front_get(url_or_path, params=None):
if params:
url += "?" + parse.urlencode(params, doseq=True)
req = request.Request(
url,
headers={
"Authorization": f"Bearer {_get_front_token()}",
"Accept": "application/json",
},
)
req = request.Request(url, headers={
"Authorization": f"Bearer {_get_front_token()}",
"Accept": "application/json",
})
time.sleep(RATE_LIMIT_DELAY)
@ -101,17 +97,12 @@ def _front_paginate(path, params=None):
# Slack API
# ---------------------------------------------------------------------------
def _slack_post(method, payload):
data = json.dumps(payload).encode()
req = request.Request(
f"{SLACK_BASE}/{method}",
data=data,
headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {_get_slack_token()}",
},
)
req = request.Request(f"{SLACK_BASE}/{method}", data=data, headers={
"Content-Type": "application/json; charset=utf-8",
"Authorization": f"Bearer {_get_slack_token()}",
})
try:
with request.urlopen(req) as resp:
@ -126,12 +117,9 @@ def _slack_post(method, payload):
def _slack_get(method, params):
url = f"{SLACK_BASE}/{method}?" + parse.urlencode(params)
req = request.Request(
url,
headers={
"Authorization": f"Bearer {_get_slack_token()}",
},
)
req = request.Request(url, headers={
"Authorization": f"Bearer {_get_slack_token()}",
})
try:
with request.urlopen(req) as resp:
@ -158,21 +146,17 @@ def _resolve_slack_user(email):
def _send_slack(channel, blocks, text):
_slack_post(
"chat.postMessage",
{
"channel": channel,
"blocks": blocks,
"text": text,
},
)
_slack_post("chat.postMessage", {
"channel": channel,
"blocks": blocks,
"text": text,
})
# ---------------------------------------------------------------------------
# Business time calculation (weekdays only, Eastern time)
# ---------------------------------------------------------------------------
def _business_minutes_elapsed(since_utc, now_utc):
since = since_utc.astimezone(EASTERN)
now = now_utc.astimezone(EASTERN)
@ -202,7 +186,6 @@ def _business_minutes_elapsed(since_utc, now_utc):
# DynamoDB dedup
# ---------------------------------------------------------------------------
def _already_alerted(conv_id, tier):
resp = _get_table().get_item(Key={"conversationId": conv_id})
item = resp.get("Item")
@ -246,7 +229,6 @@ def _record_run(today_str):
# Slack message blocks
# ---------------------------------------------------------------------------
def _tier1_blocks(conv, assignee_email=None):
subject = conv.get("subject", "No subject")
conv_id = conv.get("id", "")
@ -256,25 +238,13 @@ def _tier1_blocks(conv, assignee_email=None):
status = f"Assigned to {assignee_email}" if assignee_email else "Unassigned"
return [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":warning: SLA Breach: 1-Hour Acknowledgment",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"*<{link}|{subject}>*\n"
f"Last inbound: {ts}\n"
f"Status: {status}\n"
f"_No reply for over 1 business hour._"
),
},
},
{"type": "header", "text": {"type": "plain_text", "text": ":warning: SLA Breach: 1-Hour Acknowledgment"}},
{"type": "section", "text": {"type": "mrkdwn", "text": (
f"*<{link}|{subject}>*\n"
f"Last inbound: {ts}\n"
f"Status: {status}\n"
f"_No reply for over 1 business hour._"
)}},
]
@ -288,25 +258,13 @@ def _tier2_blocks(conv):
status = f"Assigned to {assignee['email']}" if assignee else "Unassigned"
return [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":rotating_light: SLA Breach: 1-Day Action Required",
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"*<{link}|{subject}>*\n"
f"Last inbound: {ts}\n"
f"Status: {status}\n"
f"_No reply for over 1 business day. Immediate attention required._"
),
},
},
{"type": "header", "text": {"type": "plain_text", "text": ":rotating_light: SLA Breach: 1-Day Action Required"}},
{"type": "section", "text": {"type": "mrkdwn", "text": (
f"*<{link}|{subject}>*\n"
f"Last inbound: {ts}\n"
f"Status: {status}\n"
f"_No reply for over 1 business day. Immediate attention required._"
)}},
]
@ -333,13 +291,7 @@ def _summary_blocks(tier1_breaches, tier2_breaches):
total = len(tier1_breaches) + len(tier2_breaches)
return [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f":sunrise: Morning SLA Summary — {total} breach{'es' if total != 1 else ''}",
},
},
{"type": "header", "text": {"type": "plain_text", "text": f":sunrise: Morning SLA Summary — {total} breach{'es' if total != 1 else ''}"}},
{"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}},
]
@ -348,7 +300,6 @@ def _summary_blocks(tier1_breaches, tier2_breaches):
# Handler
# ---------------------------------------------------------------------------
def handler(event, context):
ack_threshold = int(os.environ["ACK_SLA_MINUTES"])
action_threshold = int(os.environ["ACTION_SLA_MINUTES"])
@ -366,9 +317,7 @@ def handler(event, context):
return {"skipped": True, "reason": "before_start_date"}
if now_et.hour < BH_START or now_et.hour >= BH_END:
logger.info(
"Outside business hours (%s ET), skipping", now_et.strftime("%H:%M")
)
logger.info("Outside business hours (%s ET), skipping", now_et.strftime("%H:%M"))
return {"skipped": True, "reason": "outside_business_hours"}
today_str = now_et.strftime("%Y-%m-%d")
@ -387,11 +336,8 @@ def handler(event, context):
if monitor_set:
shared = [i for i in shared if i.get("name", "").lower() in monitor_set]
logger.info(
"Monitoring %d inboxes: %s",
len(shared),
", ".join(i.get("name", "?") for i in shared),
)
logger.info("Monitoring %d inboxes: %s", len(shared),
", ".join(i.get("name", "?") for i in shared))
tier1_breaches = []
tier2_breaches = []
@ -410,9 +356,7 @@ def handler(event, context):
logger.error("Failed to fetch conversations for %s: %s", inbox_name, e)
continue
logger.info(
"Inbox '%s': %d recent open conversations", inbox_name, len(conversations)
)
logger.info("Inbox '%s': %d recent open conversations", inbox_name, len(conversations))
for conv in conversations:
if conv.get("id") in seen:
@ -433,18 +377,12 @@ def handler(event, context):
logger.info("Tier 2 breach: %s", conv_id)
elif elapsed >= ack_threshold and not _already_alerted(conv_id, 1):
email = (
assignee["email"]
if assignee and assignee.get("email")
else None
)
email = assignee["email"] if assignee and assignee.get("email") else None
tier1_breaches.append((conv, email))
logger.info("Tier 1 breach: %s", conv_id)
except Exception:
logger.exception(
"Error processing conversation %s", conv.get("id", "?")
)
logger.exception("Error processing conversation %s", conv.get("id", "?"))
total = len(tier1_breaches) + len(tier2_breaches)
@ -460,27 +398,18 @@ def handler(event, context):
for conv in tier2_breaches:
adam_uid = _resolve_slack_user(adam_email)
target = adam_uid or alert_channel
_send_slack(
target,
_tier2_blocks(conv),
f"SLA Breach: {conv.get('subject', '')} - 1 day without reply",
)
_send_slack(target, _tier2_blocks(conv),
f"SLA Breach: {conv.get('subject', '')} - 1 day without reply")
for conv, email in tier1_breaches:
if email:
uid = _resolve_slack_user(email)
target = uid or alert_channel
_send_slack(
target,
_tier1_blocks(conv, email),
f"SLA Breach: {conv.get('subject', '')} - 1 hour without reply",
)
_send_slack(target, _tier1_blocks(conv, email),
f"SLA Breach: {conv.get('subject', '')} - 1 hour without reply")
else:
_send_slack(
alert_channel,
_tier1_blocks(conv),
f"SLA Breach: {conv.get('subject', '')} - unassigned, 1 hour without reply",
)
_send_slack(alert_channel, _tier1_blocks(conv),
f"SLA Breach: {conv.get('subject', '')} - unassigned, 1 hour without reply")
for conv in tier2_breaches:
_record_alert(conv["id"], 2)

View file

@ -52,18 +52,14 @@ def _list_google_users(org_units):
page_token = None
while True:
result = (
service.users()
.list(
customer="my_customer",
maxResults=500,
projection="full",
orderBy="email",
query=f"orgUnitPath='{ou}'",
pageToken=page_token,
)
.execute()
)
result = service.users().list(
customer="my_customer",
maxResults=500,
projection="full",
orderBy="email",
query=f"orgUnitPath='{ou}'",
pageToken=page_token,
).execute()
for user in result.get("users", []):
email = user.get("primaryEmail", "")
@ -75,11 +71,7 @@ def _list_google_users(org_units):
if not page_token:
break
logger.info(
" Found %d users in %s",
len([u for u in all_users if u.get("primaryEmail") in seen]),
ou,
)
logger.info(" Found %d users in %s", len([u for u in all_users if u.get("primaryEmail") in seen]), ou)
return all_users
@ -96,12 +88,8 @@ def _extract_user_fields(google_user):
phone = ""
phones = google_user.get("phones", [])
if phones:
work_phone = next(
(p for p in phones if p.get("type") == "work" and p.get("value")), None
)
primary_phone = next(
(p for p in phones if p.get("primary") and p.get("value")), None
)
work_phone = next((p for p in phones if p.get("type") == "work" and p.get("value")), None)
primary_phone = next((p for p in phones if p.get("primary") and p.get("value")), None)
fallback = next((p for p in phones if p.get("value")), None)
phone = (work_phone or primary_phone or fallback or {}).get("value", "")
@ -159,13 +147,7 @@ def handler(event, context):
google_users = _list_google_users(org_units)
logger.info("Found %d total users across %d OUs", len(google_users), len(org_units))
summary = {
"updated": 0,
"not_in_front": 0,
"no_data": 0,
"errors": 0,
"error_details": [],
}
summary = {"updated": 0, "not_in_front": 0, "no_data": 0, "errors": 0, "error_details": []}
for user in google_users:
email, job_title, phone = _extract_user_fields(user)