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