commit 660f85f1452894e557643b0b02dd4bbe4d725d90 Author: bumpsoo Date: Mon Jul 1 02:13:51 2024 +0900 node -> python 변경 진행중 diff --git a/main.py b/main.py new file mode 100644 index 0000000..c7556c2 --- /dev/null +++ b/main.py @@ -0,0 +1,79 @@ +from datetime import datetime +import json +from typing import Dict, Any, Final, List, Optional +import requests +import os +import time +from dataclasses import dataclass + +import menu +import schedule + + +# Load environment variables (recommended for sensitive data) +# SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL'] +#EXTERNAL_API_URL = os.environ['EXTERNAL_API_URL'] + +PARAM_ERROR: Final[Dict[str, str]] = { + 'statusCode': '200', 'error': 'wrong parameter' +} +MAX_RETRY: Final[int] = 5 +RETRY_INTERVAL: Final[int] = 15 + +@dataclass +class Param: + slack_url: str + date: str + count: int + +def __parse_param(evt: Dict[str, Any]) -> Optional[Param]: + if 'slack_url' not in evt: + return None + try: + date = evt['date'] + except: + date = datetime.now().strftime('%Y%m%d') + cnt = evt.get('count') or 0 + return Param(str(evt.get('slack_url')), date, cnt + 1) + +def retry(p: Param, lambda_arn: str, schedule_role_arn: str) -> Dict[str, str]: + schedule.one_time_schedule(lambda_arn, schedule_role_arn, RETRY_INTERVAL, p) + return {'statusCode': '200', 'body': f'retry {p.count}'} + +def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, str]: + os.environ['TZ'] = 'Asia/Seoul' + time.tzset() + lambda_arn = os.environ['LAMBDA_ARN'] + schedule_role_arn = os.environ['SCHEDULE_ROLE_ARN'] + param = __parse_param(event) + if param is None: + return PARAM_ERROR + menus = menu.menu(param.date) + menus_without_img = [ x for x in menus if x.menu_image is None ] + if len(menus) == 0: + return retry(param, lambda_arn, schedule_role_arn) + menus_without_img: List[menu.Menu] = [ x for x in menus if x.menu_image is None ] + if len(menus_without_img) > 0 and param.count < MAX_RETRY: + return retry(param, lambda_arn, schedule_role_arn) + # Extract relevant data for your Slack message (adjust as needed) + message_content: str = f"*Important Data from External API*\n" + message_content += f"`\n{json.dumps(api_data, indent=4)}\n`" + # Construct payload for the Slack webhook + slack_payload: Dict[str, Any] = { + "text": message_content, + "blocks": [ + {"type": "section", "text": {"type": "mrkdwn", "text": message_content}} + ] + } + # Send the message to Slack + slack_response: requests.Response = requests.post( + '', + #SLACK_WEBHOOK_URL, + data=json.dumps(slack_payload), + headers={'Content-Type': 'application/json'} + ) + slack_response.raise_for_status() + return { + 'statusCode': '200', + 'body': json.dumps('Data successfully sent to Slack.') + } diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..234d4d3 --- /dev/null +++ b/main.tf @@ -0,0 +1,95 @@ +# Provider Configuration +provider "aws" { + region = "ap-northeast-2" +} + +# Locals for Constants (replace values as needed) +locals { + prefix = "bumpsoo-menu" + image_bucket_name = "${local.prefix}-img-bucket" + lambda_role_name = "${local.prefix}-lambda-role" + lambda_function_name = "${local.prefix}-lambda" + lambda_filename = "artifacts.zip" # Zip file containing Lambda code + lambda_handler = "lambda_function.lambda_handler" # Replace with your handler + weekday_rule_name = "${local.prefix}-weekday-image-upload" +} + +# S3 Bucket (Publicly Accessible) +resource "aws_s3_bucket" "image_bucket" { + bucket = local.image_bucket_name + acl = "public-read" + + # Policy for public read access to objects + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "PublicReadGetObject" + Effect = "Allow" + Principal = "*" + Action = "s3:GetObject" + Resource = "arn:aws:s3:::${aws_s3_bucket.image_bucket.bucket}/*" + } + ] + }) +} + +# IAM Role for Lambda (EventBridge Permissions) +resource "aws_iam_role" "lambda_role" { + name = local.lambda_role_name + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Principal = { + Service = "lambda.amazonaws.com" + } + Effect = "Allow" + } + ] + }) + + # Policy to allow EventBridge rule creation/management + inline_policy { + name = "lambda_eventbridge_policy" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "events:PutRule", + "events:PutTargets" + ] + Resource = "*" + } + ] + }) + } +} + +# Lambda Function +resource "aws_lambda_function" "image_lambda" { + function_name = local.lambda_function_name + filename = local.lambda_filename + role = aws_iam_role.lambda_role.arn + handler = local.lambda_handler + runtime = "python3.11" +} + +# EventBridge Rule +resource "aws_cloudwatch_event_rule" "weekday_rule" { + name = local.weekday_rule_name + description = "Trigger Lambda at 10 AM on weekdays" + schedule_expression = "cron(0 10 ? * MON-FRI *)" # 10 AM every workday in KST timezone +} + +# EventBridge Target (Lambda) +resource "aws_cloudwatch_event_target" "lambda_target" { + rule = aws_cloudwatch_event_rule.weekday_rule.name + target_id = "lambda" + arn = aws_lambda_function.image_lambda.arn +} + diff --git a/menu.py b/menu.py new file mode 100644 index 0000000..dfce087 --- /dev/null +++ b/menu.py @@ -0,0 +1,63 @@ +# menu repository +from dataclasses import dataclass +from typing import Any, Dict, Final, List, Optional +import requests + +import slack + +BASE: Final[str] = 'https://sfmn.shinsegaefood.com/' +URL: Final[str] = f'{BASE}selectTodayMenu2.do' +HEADER: Final[Dict[str, str]] = { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1', + 'Content-Type': 'application/json', + 'Referer': 'https://sfmn.shinsegaefood.com/selectTodayMenu.do', +} +STORE: Final[str] = '06379' + +@dataclass +class Menu: + meal_time: str + meal_type: str + rep_menu: str + detailed_menu: str + calory: str + menu_image: Optional[str] + + def __post_init__(self): + if self.menu_image: + self.menu_image = f"https://store.shinsegaefood.com/{self.menu_image}" + + def to_slack_block(self, date: str) -> Dict[str, Any]: + blocks = [slack.markdown(f'*{date} 일자 메뉴*')] + return {} + +# date should be this form 20240601 +def menu(date: str) -> List[Menu]: + menus: List[Menu] = [] + res = requests.post(URL, headers=HEADER, json={ + 'menuDate': date, + 'storeCd': STORE, + 'cafeCd': '01', + 'dispBaseCd': '0', + 'userLang': 'K' + }) + if res.status_code != 200: + return menus + data: List[Dict[str, str]] = [] + try: + data = res.json()['model']['model'] + except: + return menus + for each in data: + try: + menus.append(Menu( + each['MEAL_TYPE_NM'], + each['DINNER_TYPE_NM'], + each['REP_TYPE_NM'], + each['MENU_DESC'], + each['TOT_CALORY'], + each.get('MEAL_TYPE_NM'), + )) + except: + pass + return menus diff --git a/schedule.py b/schedule.py new file mode 100644 index 0000000..59c5e1c --- /dev/null +++ b/schedule.py @@ -0,0 +1,38 @@ +import boto3 +from datetime import datetime, timedelta +import json +import main + +def one_time_schedule( + lambda_arn: str, + schedule_role_arn: str, + minutes: int, + param: main.Param +) -> str: + client = boto3.client('scheduler') + schedule_time = datetime.now() + timedelta(minutes = minutes) + schedule_expression = f"at({schedule_time.isoformat()})" + + schedule_name = f"menu-trigger-{schedule_time.strftime('%Y%m%d%H%M%S')}" + rule_response = client.create_schedule( + Name = schedule_name, + ScheduleExpression = schedule_expression, + ScheduleExpressionTimezone = 'Asia/Seoul', + State = 'ENABLED', + Target = { + 'Arn': lambda_arn, + 'RoleArn': schedule_role_arn, + 'Input': json.dumps({ + 'slack_url': param.slack_url, + 'date': param.date, + 'count': param.count + 1 + }), + 'RetryPolicy': {'MaximumRetryAttempts': 0} + }, + ) + client.update_schedule( + Name = schedule_name, + ActionAfterCompletion = 'DELETE' + ) + return rule_response['RuleArn'] + diff --git a/slack.py b/slack.py new file mode 100644 index 0000000..400763c --- /dev/null +++ b/slack.py @@ -0,0 +1,10 @@ +from typing import Any, Dict + +def markdown(text: str) -> Dict[str, Any]: + return { + 'type': 'section', + 'text': { + 'type': 'mrkdwn', + 'text': text + } + }