node -> python 변경 진행중
This commit is contained in:
commit
660f85f145
5 changed files with 285 additions and 0 deletions
79
main.py
Normal file
79
main.py
Normal file
|
|
@ -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.')
|
||||
}
|
||||
95
main.tf
Normal file
95
main.tf
Normal file
|
|
@ -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
|
||||
}
|
||||
|
||||
63
menu.py
Normal file
63
menu.py
Normal file
|
|
@ -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
|
||||
38
schedule.py
Normal file
38
schedule.py
Normal file
|
|
@ -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']
|
||||
|
||||
10
slack.py
Normal file
10
slack.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
def markdown(text: str) -> Dict[str, Any]:
|
||||
return {
|
||||
'type': 'section',
|
||||
'text': {
|
||||
'type': 'mrkdwn',
|
||||
'text': text
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue