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