Add CI workflow and apply ruff formatting (#10)

This commit is contained in:
Adam Moussa 2026-05-08 15:51:22 -04:00 • committed by GitHub
parent 885379f85d
commit 853fb3253f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 50 additions and 20 deletions

12
.github/workflows/ci.yaml vendored Normal file
View file

@ -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

View file

@ -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"),
)

View file

@ -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"
)

View file

@ -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}")

View file

@ -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"}

View file

@ -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"<strong>{commenter}</strong> &mdash; " if commenter else ""
commenter_str = (
f"<strong>{commenter}</strong> &mdash; " 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")),

View file

@ -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