From 853fb3253f61f854e7abe5fa602ffbcaa6284c02 Mon Sep 17 00:00:00 2001
From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com>
Date: Fri, 8 May 2026 15:51:22 -0400
Subject: [PATCH] Add CI workflow and apply ruff formatting (#10)
---
.github/workflows/ci.yaml | 12 +++++++++++
cdk/app.py | 4 +++-
cdk/stack.py | 32 ++++++++++++++++++++----------
lambdas/email_processor/handler.py | 5 +++--
lambdas/vendor_reply/handler.py | 5 +++--
lambdas/web_ui/handler.py | 11 ++++++----
shared/models.py | 1 +
7 files changed, 50 insertions(+), 20 deletions(-)
create mode 100644 .github/workflows/ci.yaml
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..35cc21d
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,12 @@
+name: CI
+on:
+ pull_request:
+ branches: [main]
+
+jobs:
+ ci:
+ uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@main
+ with:
+ source-dirs: "lambdas cdk shared"
+ run-cdk-synth: true
+ run-sam-validate: false
diff --git a/cdk/app.py b/cdk/app.py
index 108b84a..d324f57 100644
--- a/cdk/app.py
+++ b/cdk/app.py
@@ -3,7 +3,9 @@ import aws_cdk as cdk
from stack import WorkorderIngestStack
app = cdk.App()
-WorkorderIngestStack(app, "workorder-ingest",
+WorkorderIngestStack(
+ app,
+ "workorder-ingest",
stack_name="WorkorderIngestStack",
env=cdk.Environment(region="us-east-1"),
)
diff --git a/cdk/stack.py b/cdk/stack.py
index 2770fc1..1532be0 100644
--- a/cdk/stack.py
+++ b/cdk/stack.py
@@ -12,7 +12,6 @@ from aws_cdk import (
aws_ses as ses,
aws_ses_actions as ses_actions,
aws_secretsmanager as secretsmanager,
- aws_iam as iam,
)
from constructs import Construct
@@ -23,7 +22,8 @@ class WorkorderIngestStack(Stack):
# --- S3 bucket for raw emails ---
email_bucket = s3.Bucket(
- self, "EmailBucket",
+ self,
+ "EmailBucket",
bucket_name=f"workorder-ingest-emails-{self.account}",
removal_policy=RemovalPolicy.RETAIN,
lifecycle_rules=[
@@ -33,7 +33,8 @@ class WorkorderIngestStack(Stack):
# --- DynamoDB tables ---
work_orders_table = dynamodb.Table(
- self, "WorkOrdersTable",
+ self,
+ "WorkOrdersTable",
table_name="WorkOrders",
partition_key=dynamodb.Attribute(
name="work_order_id",
@@ -68,7 +69,8 @@ class WorkorderIngestStack(Stack):
)
comments_table = dynamodb.Table(
- self, "CommentsTable",
+ self,
+ "CommentsTable",
table_name="WorkOrderComments",
partition_key=dynamodb.Attribute(
name="work_order_id",
@@ -84,14 +86,16 @@ class WorkorderIngestStack(Stack):
# --- Secrets Manager for Anthropic API key ---
anthropic_secret = secretsmanager.Secret(
- self, "AnthropicApiKey",
+ self,
+ "AnthropicApiKey",
secret_name="workorder-ingest/anthropic-api-key",
description="Anthropic API key for work order email parsing",
)
# --- Lambda function ---
email_processor = lambda_.Function(
- self, "EmailProcessor",
+ self,
+ "EmailProcessor",
function_name="workorder-email-processor",
runtime=lambda_.Runtime.PYTHON_3_12,
handler="handler.handler",
@@ -101,10 +105,11 @@ class WorkorderIngestStack(Stack):
image=lambda_.Runtime.PYTHON_3_12.bundling_image,
platform="linux/amd64",
command=[
- "bash", "-c",
+ "bash",
+ "-c",
"pip install --platform manylinux2014_x86_64 --only-binary=:all: "
"-r requirements.txt -t /asset-output && "
- "cp -r . /asset-output/"
+ "cp -r . /asset-output/",
],
),
),
@@ -132,7 +137,9 @@ class WorkorderIngestStack(Stack):
# --- SES Receipt Rule ---
rule_set = ses.ReceiptRuleSet.from_receipt_rule_set_name(
- self, "ExistingRuleSet", "INBOUND_MAIL",
+ self,
+ "ExistingRuleSet",
+ "INBOUND_MAIL",
)
rule_set.add_rule(
@@ -148,7 +155,8 @@ class WorkorderIngestStack(Stack):
# --- Web UI Lambda ---
web_ui = lambda_.Function(
- self, "WebUI",
+ self,
+ "WebUI",
function_name="workorder-web-ui",
runtime=lambda_.Runtime.PYTHON_3_12,
handler="handler.handler",
@@ -169,4 +177,6 @@ class WorkorderIngestStack(Stack):
auth_type=lambda_.FunctionUrlAuthType.NONE,
)
- cdk.CfnOutput(self, "WebUIUrl", value=web_url.url, description="Work Order Dashboard URL")
+ cdk.CfnOutput(
+ self, "WebUIUrl", value=web_url.url, description="Work Order Dashboard URL"
+ )
diff --git a/lambdas/email_processor/handler.py b/lambdas/email_processor/handler.py
index ad78dc7..2abdc8f 100644
--- a/lambdas/email_processor/handler.py
+++ b/lambdas/email_processor/handler.py
@@ -13,7 +13,6 @@ import os
import re
from datetime import datetime
from email import policy
-from typing import Optional
import anthropic
import boto3
@@ -253,7 +252,9 @@ def handler(event, context):
# Extract structured data with Claude
parsed = extract_with_claude(email_data)
- logger.info(f"Parsed: type={parsed.get('email_type')}, wo={parsed.get('work_order_id')}")
+ logger.info(
+ f"Parsed: type={parsed.get('email_type')}, wo={parsed.get('work_order_id')}"
+ )
if not parsed.get("work_order_id"):
logger.warning(f"No work order ID found in email, skipping: {key}")
diff --git a/lambdas/vendor_reply/handler.py b/lambdas/vendor_reply/handler.py
index 72ee313..05e7428 100644
--- a/lambdas/vendor_reply/handler.py
+++ b/lambdas/vendor_reply/handler.py
@@ -1,5 +1,4 @@
import email
-import json
import logging
import os
import re
@@ -120,6 +119,8 @@ def handler(event, context):
}
)
- logger.info(f"Saved vendor reply: WO={wo_number} DSP={dispatch_number} from={sender_email}")
+ logger.info(
+ f"Saved vendor reply: WO={wo_number} DSP={dispatch_number} from={sender_email}"
+ )
return {"statusCode": 200, "body": "OK"}
diff --git a/lambdas/web_ui/handler.py b/lambdas/web_ui/handler.py
index 3abc7de..3b32d8d 100644
--- a/lambdas/web_ui/handler.py
+++ b/lambdas/web_ui/handler.py
@@ -5,9 +5,7 @@ Serves a simple HTML dashboard for viewing work orders and comments.
Accessed via Lambda Function URL.
"""
-import json
import os
-import urllib.parse
import boto3
@@ -71,7 +69,9 @@ def render_work_order_detail(wo, events):
created = e.get("created_at", "")
text = e.get("text", "")
badge = render_badge(record_type, RECORD_TYPE_COLORS)
- commenter_str = f"{commenter} — " if commenter else ""
+ commenter_str = (
+ f"{commenter} — " if commenter else ""
+ )
border_colors = {
"new_work_order": "#3b82f6",
@@ -95,7 +95,10 @@ def render_work_order_detail(wo, events):
fields = [
("Description", wo.get("description")),
("Status", render_badge(wo.get("wo_status", "unknown"), STATUS_COLORS)),
- ("Record Type", render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS)),
+ (
+ "Record Type",
+ render_badge(wo.get("record_type", "unknown"), RECORD_TYPE_COLORS),
+ ),
("Site Code", wo.get("site_code")),
("Building", wo.get("building")),
("Address", wo.get("address")),
diff --git a/shared/models.py b/shared/models.py
index cb50e29..22374c0 100644
--- a/shared/models.py
+++ b/shared/models.py
@@ -63,6 +63,7 @@ class WorkOrder:
@dataclass
class ParsedEmail:
"""Result of AI parsing an inbound email."""
+
email_type: str # EmailType value
work_order_id: Optional[str] = None
description: Optional[str] = None