기본 구조 완성. 파일 정리하면 끝. 일단 배포 완료
todo - tf 파일의 slack_url 환경변수 등으로 정리 - 배포하는 스크립트? 필요할 것 같기도 함
This commit is contained in:
parent
91e986b475
commit
1b8b580554
6 changed files with 230 additions and 82 deletions
53
main.py
53
main.py
|
|
@ -1,13 +1,12 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
from typing import Dict, Any, Final, Optional
|
||||||
from typing import Dict, Any, Final, List, Optional
|
|
||||||
import requests
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import menu
|
import menu
|
||||||
import schedule
|
import schedule
|
||||||
|
import slack
|
||||||
|
|
||||||
|
|
||||||
# Load environment variables (recommended for sensitive data)
|
# Load environment variables (recommended for sensitive data)
|
||||||
|
|
@ -37,43 +36,35 @@ def __parse_param(evt: Dict[str, Any]) -> Optional[Param]:
|
||||||
return Param(str(evt.get('slack_url')), date, cnt + 1)
|
return Param(str(evt.get('slack_url')), date, cnt + 1)
|
||||||
|
|
||||||
def retry(p: Param, lambda_arn: str, schedule_role_arn: str) -> Dict[str, str]:
|
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)
|
schedule.one_time_schedule(lambda_arn, schedule_role_arn, RETRY_INTERVAL, p.slack_url, p.date, p.count)
|
||||||
return {'statusCode': '200', 'body': f'retry {p.count}'}
|
return {'statusCode': '200', 'body': f'retry {p.count}'}
|
||||||
|
|
||||||
|
def wrap_return(ret: bool) -> Dict[str, str]:
|
||||||
|
return {
|
||||||
|
'statusCode': '200',
|
||||||
|
'body': 'success' if ret else 'fail'
|
||||||
|
}
|
||||||
|
|
||||||
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, str]:
|
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, str]:
|
||||||
os.environ['TZ'] = 'Asia/Seoul'
|
os.environ['TZ'] = 'Asia/Seoul'
|
||||||
time.tzset()
|
time.tzset()
|
||||||
lambda_arn = os.environ['LAMBDA_ARN']
|
if context.invoked_function_arn is None:
|
||||||
|
raise Exception('None???')
|
||||||
|
lambda_arn = str(context.invoked_function_arn)
|
||||||
schedule_role_arn = os.environ['SCHEDULE_ROLE_ARN']
|
schedule_role_arn = os.environ['SCHEDULE_ROLE_ARN']
|
||||||
|
bucket_name = os.environ['S3_BUCKET_NAME']
|
||||||
param = __parse_param(event)
|
param = __parse_param(event)
|
||||||
if param is None:
|
if param is None:
|
||||||
return PARAM_ERROR
|
return PARAM_ERROR
|
||||||
menus = menu.menu(param.date)
|
menus = menu.menu(param.date)
|
||||||
menus_without_img = [ x for x in menus if x.menu_image is None ]
|
menus_without_img = [ x for x in menus if x.menu_image is None ]
|
||||||
if param.count < MAX_RETRY and (len(menus) == 0 or len(menus_without_img) > 0):
|
print('menus', menus)
|
||||||
|
print('menus_without_img', menus_without_img)
|
||||||
|
print('param', param)
|
||||||
|
if param.count <= MAX_RETRY and (len(menus) == 0 or len(menus_without_img) > 0):
|
||||||
return retry(param, lambda_arn, schedule_role_arn)
|
return retry(param, lambda_arn, schedule_role_arn)
|
||||||
if len(menus) == 0:
|
|
||||||
# 슬랙 메시지 전송( 오늘의 메뉴 존재 X )
|
for each in menus:
|
||||||
return retry(param, lambda_arn, schedule_role_arn)
|
each.resize_image(bucket_name, param.date)
|
||||||
# Extract relevant data for your Slack message (adjust as needed)
|
return wrap_return(slack.send(param.slack_url, param.date, menus))
|
||||||
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.')
|
|
||||||
}
|
|
||||||
|
|
|
||||||
141
main.tf
141
main.tf
|
|
@ -10,29 +10,49 @@ locals {
|
||||||
lambda_role_name = "${local.prefix}-lambda-role"
|
lambda_role_name = "${local.prefix}-lambda-role"
|
||||||
lambda_function_name = "${local.prefix}-lambda"
|
lambda_function_name = "${local.prefix}-lambda"
|
||||||
lambda_filename = "artifacts.zip" # Zip file containing Lambda code
|
lambda_filename = "artifacts.zip" # Zip file containing Lambda code
|
||||||
lambda_handler = "lambda_function.lambda_handler" # Replace with your handler
|
lambda_handler = "main.lambda_handler" # Replace with your handler
|
||||||
weekday_rule_name = "${local.prefix}-weekday-image-upload"
|
weekday_rule_name = "${local.prefix}-weekday-image-upload"
|
||||||
|
|
||||||
|
schedule_role_name = "${local.prefix}-schedule-role"
|
||||||
}
|
}
|
||||||
|
|
||||||
# S3 Bucket (Publicly Accessible)
|
# S3 Bucket (Publicly Accessible)
|
||||||
resource "aws_s3_bucket" "image_bucket" {
|
resource "aws_s3_bucket" "image_bucket" {
|
||||||
bucket = local.image_bucket_name
|
bucket = local.image_bucket_name
|
||||||
acl = "public-read"
|
}
|
||||||
|
|
||||||
# Policy for public read access to objects
|
# S3 Bucket Public Access Block (disabled for objects)
|
||||||
policy = jsonencode({
|
resource "aws_s3_bucket_public_access_block" "image_bucket_public_access_block" {
|
||||||
Version = "2012-10-17"
|
bucket = aws_s3_bucket.image_bucket.id
|
||||||
Statement = [
|
block_public_acls = false # Block public ACLs
|
||||||
|
block_public_policy = false # Block public bucket policies
|
||||||
|
ignore_public_acls = false # Ignore public ACLs on existing objects
|
||||||
|
restrict_public_buckets = false # Restrict public bucket policies on existing buckets
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_s3_bucket_policy" "image_bucket_policy" {
|
||||||
|
depends_on = [
|
||||||
|
aws_s3_bucket.image_bucket,
|
||||||
|
aws_s3_bucket_public_access_block.image_bucket_public_access_block
|
||||||
|
]
|
||||||
|
bucket = aws_s3_bucket.image_bucket.id
|
||||||
|
policy = <<EOF
|
||||||
{
|
{
|
||||||
Sid = "PublicReadGetObject"
|
"Version": "2012-10-17",
|
||||||
Effect = "Allow"
|
"Statement": [
|
||||||
Principal = "*"
|
{
|
||||||
Action = "s3:GetObject"
|
"Sid": "PublicReadGetObject",
|
||||||
Resource = "arn:aws:s3:::${aws_s3_bucket.image_bucket.bucket}/*"
|
"Effect": "Allow",
|
||||||
|
"Principal": "*",
|
||||||
|
"Action": "s3:GetObject",
|
||||||
|
"Resource": "arn:aws:s3:::bumpsoo-menu-img-bucket/*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# IAM Role for Lambda (EventBridge Permissions)
|
# IAM Role for Lambda (EventBridge Permissions)
|
||||||
resource "aws_iam_role" "lambda_role" {
|
resource "aws_iam_role" "lambda_role" {
|
||||||
|
|
@ -60,8 +80,29 @@ resource "aws_iam_role" "lambda_role" {
|
||||||
{
|
{
|
||||||
Effect = "Allow"
|
Effect = "Allow"
|
||||||
Action = [
|
Action = [
|
||||||
"events:PutRule",
|
"scheduler:CreateSchedule",
|
||||||
"events:PutTargets"
|
"scheduler:UpdateSchedule",
|
||||||
|
]
|
||||||
|
Resource = "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"iam:PassRole",
|
||||||
|
]
|
||||||
|
Resource = "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = "s3:PutObject"
|
||||||
|
Resource = "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"logs:CreateLogStream",
|
||||||
|
"logs:CreateLogGroup",
|
||||||
|
"logs:PutLogEvents"
|
||||||
]
|
]
|
||||||
Resource = "*"
|
Resource = "*"
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +111,45 @@ resource "aws_iam_role" "lambda_role" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# IAM Role for Lambda (EventBridge Permissions)
|
||||||
|
resource "aws_iam_role" "schedule_role" {
|
||||||
|
name = local.schedule_role_name
|
||||||
|
|
||||||
|
assume_role_policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Action = "sts:AssumeRole"
|
||||||
|
Principal = {
|
||||||
|
Service = "scheduler.amazonaws.com"
|
||||||
|
}
|
||||||
|
Effect = "Allow"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
# Policy to allow EventBridge rule creation/management
|
||||||
|
inline_policy {
|
||||||
|
name = "schedule_invoke_any_lambda"
|
||||||
|
policy = jsonencode({
|
||||||
|
Version = "2012-10-17"
|
||||||
|
Statement = [
|
||||||
|
{
|
||||||
|
Effect = "Allow"
|
||||||
|
Action = [
|
||||||
|
"lambda:InvokeFunction"
|
||||||
|
]
|
||||||
|
Resource = "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "aws_cloudwatch_log_group" "image_lambda_log_group" {
|
||||||
|
name = "/aws/lambda/${local.lambda_function_name}"
|
||||||
|
retention_in_days = 14 # Adjust retention period as needed
|
||||||
|
}
|
||||||
# Lambda Function
|
# Lambda Function
|
||||||
resource "aws_lambda_function" "image_lambda" {
|
resource "aws_lambda_function" "image_lambda" {
|
||||||
function_name = local.lambda_function_name
|
function_name = local.lambda_function_name
|
||||||
|
|
@ -77,19 +157,34 @@ resource "aws_lambda_function" "image_lambda" {
|
||||||
role = aws_iam_role.lambda_role.arn
|
role = aws_iam_role.lambda_role.arn
|
||||||
handler = local.lambda_handler
|
handler = local.lambda_handler
|
||||||
runtime = "python3.11"
|
runtime = "python3.11"
|
||||||
|
timeout = 60
|
||||||
|
|
||||||
|
environment {
|
||||||
|
variables = {
|
||||||
|
SCHEDULE_ROLE_ARN = aws_iam_role.schedule_role.arn
|
||||||
|
S3_BUCKET_NAME = local.image_bucket_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
depends_on = [aws_cloudwatch_log_group.image_lambda_log_group]
|
||||||
}
|
}
|
||||||
|
|
||||||
# EventBridge Rule
|
|
||||||
resource "aws_cloudwatch_event_rule" "weekday_rule" {
|
resource "aws_scheduler_schedule" "weekday_schedule" {
|
||||||
name = local.weekday_rule_name
|
name = local.weekday_rule_name # Keep the same name
|
||||||
description = "Trigger Lambda at 10 AM on weekdays"
|
description = "Trigger Lambda at 10 AM on weekdays (KST)"
|
||||||
schedule_expression = "cron(0 10 ? * MON-FRI *)" # 10 AM every workday in KST timezone
|
schedule_expression = "cron(30 10 ? * MON-FRI *)"
|
||||||
|
schedule_expression_timezone = "Asia/Seoul"
|
||||||
|
flexible_time_window {
|
||||||
|
mode = "OFF"
|
||||||
}
|
}
|
||||||
|
|
||||||
# EventBridge Target (Lambda)
|
target {
|
||||||
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
|
arn = aws_lambda_function.image_lambda.arn
|
||||||
|
role_arn = aws_iam_role.schedule_role.arn
|
||||||
|
|
||||||
|
input = <<EOF
|
||||||
|
{"slack_url": "https://hooks.slack.com/services/"}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
39
menu.py
39
menu.py
|
|
@ -1,9 +1,13 @@
|
||||||
# menu repository
|
# menu repository
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, Final, List, Optional
|
from datetime import datetime
|
||||||
|
from typing import Dict, Final, List, Optional
|
||||||
import requests
|
import requests
|
||||||
|
from PIL import Image
|
||||||
|
from io import BytesIO
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
import slack
|
|
||||||
|
|
||||||
BASE: Final[str] = 'https://sfmn.shinsegaefood.com/'
|
BASE: Final[str] = 'https://sfmn.shinsegaefood.com/'
|
||||||
URL: Final[str] = f'{BASE}selectTodayMenu2.do'
|
URL: Final[str] = f'{BASE}selectTodayMenu2.do'
|
||||||
|
|
@ -26,6 +30,31 @@ class Menu:
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.menu_image:
|
if self.menu_image:
|
||||||
self.menu_image = f"https://store.shinsegaefood.com/{self.menu_image}"
|
self.menu_image = f"https://store.shinsegaefood.com/{self.menu_image}"
|
||||||
|
def resize_image(self, bucket_name: str, date: str):
|
||||||
|
if self.menu_image is None:
|
||||||
|
return
|
||||||
|
image_url = self.menu_image
|
||||||
|
filename: str = date + '/' + str(datetime.now().timestamp()) + 'png'
|
||||||
|
response = requests.get(image_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
target_size = 1024 * 1024
|
||||||
|
image_data = response.content
|
||||||
|
img = Image.open(BytesIO(image_data))
|
||||||
|
while len(image_data) > target_size:
|
||||||
|
resize_factor = 0.9
|
||||||
|
new_width, new_height = int(img.width * resize_factor), int(img.height * resize_factor)
|
||||||
|
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||||
|
img_bytes = BytesIO()
|
||||||
|
img.save(img_bytes, format='png')
|
||||||
|
image_data = img_bytes.getvalue()
|
||||||
|
s3 = boto3.client("s3")
|
||||||
|
s3.put_object(
|
||||||
|
Body=image_data,
|
||||||
|
Bucket=bucket_name,
|
||||||
|
Key=filename,
|
||||||
|
)
|
||||||
|
self.menu_image = f"https://{bucket_name}.s3.amazonaws.com/{filename}"
|
||||||
|
|
||||||
|
|
||||||
# date should be this form 20240601
|
# date should be this form 20240601
|
||||||
def menu(date: str) -> List[Menu]:
|
def menu(date: str) -> List[Menu]:
|
||||||
|
|
@ -42,6 +71,7 @@ def menu(date: str) -> List[Menu]:
|
||||||
data: List[Dict[str, str]] = []
|
data: List[Dict[str, str]] = []
|
||||||
try:
|
try:
|
||||||
data = res.json()['model']['model']
|
data = res.json()['model']['model']
|
||||||
|
print('data', data)
|
||||||
except:
|
except:
|
||||||
return menus
|
return menus
|
||||||
for each in data:
|
for each in data:
|
||||||
|
|
@ -49,11 +79,12 @@ def menu(date: str) -> List[Menu]:
|
||||||
menus.append(Menu(
|
menus.append(Menu(
|
||||||
each['MEAL_TYPE_NM'],
|
each['MEAL_TYPE_NM'],
|
||||||
each['DINNER_TYPE_NM'],
|
each['DINNER_TYPE_NM'],
|
||||||
each['REP_TYPE_NM'],
|
each['REP_MENU_NM'],
|
||||||
each['MENU_DESC'],
|
each['MENU_DESC'],
|
||||||
each['TOT_CALORY'],
|
each['TOT_CALORY'],
|
||||||
each.get('MEAL_TYPE_NM'),
|
each.get('WEB_LINK'),
|
||||||
))
|
))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return menus
|
return menus
|
||||||
|
|
||||||
|
|
|
||||||
14
requirements.txt
Normal file
14
requirements.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
boto3==1.34.136
|
||||||
|
botocore==1.34.136
|
||||||
|
certifi==2024.6.2
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
idna==3.7
|
||||||
|
jmespath==1.0.1
|
||||||
|
nodeenv==1.9.1
|
||||||
|
pillow==10.4.0
|
||||||
|
pyright==1.1.369
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
requests==2.32.3
|
||||||
|
s3transfer==0.10.2
|
||||||
|
six==1.16.0
|
||||||
|
urllib3==2.2.2
|
||||||
39
schedule.py
39
schedule.py
|
|
@ -1,38 +1,43 @@
|
||||||
import boto3
|
import boto3
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
import main
|
|
||||||
|
|
||||||
def one_time_schedule(
|
def one_time_schedule(
|
||||||
lambda_arn: str,
|
lambda_arn: str,
|
||||||
schedule_role_arn: str,
|
schedule_role_arn: str,
|
||||||
minutes: int,
|
minutes: int,
|
||||||
param: main.Param
|
slack_url: str,
|
||||||
|
date: str,
|
||||||
|
count: int,
|
||||||
) -> str:
|
) -> str:
|
||||||
client = boto3.client('scheduler')
|
client = boto3.client('scheduler')
|
||||||
schedule_time = datetime.now() + timedelta(minutes = minutes)
|
schedule_time = datetime.now() + timedelta(minutes = minutes)
|
||||||
schedule_expression = f"at({schedule_time.isoformat()})"
|
schedule_expression = f"at({schedule_time.strftime('%Y-%m-%dT%H:%M:%S')})"
|
||||||
|
|
||||||
schedule_name = f"menu-trigger-{schedule_time.strftime('%Y%m%d%H%M%S')}"
|
schedule_name = f"menu-trigger-{schedule_time.strftime('%Y%m%d%H%M%S')}"
|
||||||
|
target = {
|
||||||
|
'Arn': lambda_arn,
|
||||||
|
'RoleArn': schedule_role_arn,
|
||||||
|
'Input': json.dumps({
|
||||||
|
'slack_url': slack_url,
|
||||||
|
'date': date,
|
||||||
|
'count': count
|
||||||
|
}),
|
||||||
|
'RetryPolicy': {'MaximumRetryAttempts': 0}
|
||||||
|
}
|
||||||
rule_response = client.create_schedule(
|
rule_response = client.create_schedule(
|
||||||
Name = schedule_name,
|
Name = schedule_name,
|
||||||
ScheduleExpression = schedule_expression,
|
ScheduleExpression = schedule_expression,
|
||||||
ScheduleExpressionTimezone = 'Asia/Seoul',
|
ScheduleExpressionTimezone = 'Asia/Seoul',
|
||||||
State = 'ENABLED',
|
State = 'ENABLED',
|
||||||
Target = {
|
FlexibleTimeWindow = {'Mode': 'OFF'},
|
||||||
'Arn': lambda_arn,
|
Target = target
|
||||||
'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(
|
rule_response = client.update_schedule(
|
||||||
Name = schedule_name,
|
Name = schedule_name,
|
||||||
ActionAfterCompletion = 'DELETE'
|
ScheduleExpression = schedule_expression,
|
||||||
|
FlexibleTimeWindow = {'Mode': 'OFF'},
|
||||||
|
ActionAfterCompletion = 'DELETE',
|
||||||
|
Target = target
|
||||||
)
|
)
|
||||||
return rule_response['RuleArn']
|
return rule_response['ScheduleArn']
|
||||||
|
|
||||||
|
|
|
||||||
12
slack.py
12
slack.py
|
|
@ -1,4 +1,6 @@
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import requests
|
||||||
from menu import Menu
|
from menu import Menu
|
||||||
|
|
||||||
def markdown(text: str) -> Dict[str, Any]:
|
def markdown(text: str) -> Dict[str, Any]:
|
||||||
|
|
@ -33,6 +35,9 @@ def menu_to_str(m: Menu) -> str:
|
||||||
|
|
||||||
def for_slack(date: str, data: List[Menu]) -> Dict[str, Any]:
|
def for_slack(date: str, data: List[Menu]) -> Dict[str, Any]:
|
||||||
blocks = [markdown(f'*{date} 일자 메뉴*')]
|
blocks = [markdown(f'*{date} 일자 메뉴*')]
|
||||||
|
if len(data) == 0:
|
||||||
|
blocks.append(markdown('메뉴가 존재하지 않습니다'))
|
||||||
|
return {'blocks': blocks}
|
||||||
for each in data:
|
for each in data:
|
||||||
blocks.append(markdown(menu_to_str(each)))
|
blocks.append(markdown(menu_to_str(each)))
|
||||||
if each.menu_image:
|
if each.menu_image:
|
||||||
|
|
@ -41,3 +46,10 @@ def for_slack(date: str, data: List[Menu]) -> Dict[str, Any]:
|
||||||
blocks.append(markdown('이미지가 존재하지 않습니다'))
|
blocks.append(markdown('이미지가 존재하지 않습니다'))
|
||||||
return {'blocks': blocks}
|
return {'blocks': blocks}
|
||||||
|
|
||||||
|
def send(slack_url: str, date: str, data: List[Menu]) -> bool:
|
||||||
|
payload = for_slack(date, data)
|
||||||
|
print(payload)
|
||||||
|
res = requests.post(url= slack_url, json=payload)
|
||||||
|
print(res.text)
|
||||||
|
return res.text == 'ok'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue