From 813693748ebc83745dfedd0abd17b6fc47279ea7 Mon Sep 17 00:00:00 2001 From: Cycrypto Date: Mon, 3 Jun 2024 23:51:29 +0900 Subject: [PATCH] feat: connect data to s3 bucket --- events/meal/meal_event.json | 2 +- packaged.yaml | 2 +- sandol/api_server/meal.py | 34 +++++------- sandol/api_server/utils.py | 26 ++++----- sandol/bucket/__init__.py | 0 sandol/bucket/common.py | 30 ++++++++++ sandol/bucket/meal_bucket.py | 12 ++++ sandol/crawler/cafeteria.py | 26 +++++---- sandol/crawler/ibookcrawler.py | 92 +++++++++++++++---------------- sandol/crawler/ibookdownloader.py | 8 ++- sandol/crawler/test.json | 18 +++++- 11 files changed, 149 insertions(+), 101 deletions(-) create mode 100644 sandol/bucket/__init__.py create mode 100644 sandol/bucket/common.py create mode 100644 sandol/bucket/meal_bucket.py diff --git a/events/meal/meal_event.json b/events/meal/meal_event.json index e574bb7f..fa9dc443 100644 --- a/events/meal/meal_event.json +++ b/events/meal/meal_event.json @@ -15,6 +15,6 @@ "httpMethod": "POST", "path": "/prod/meal/view" }, - "body": "{\"intent\":{\"id\":\"viqkzodj54puyo8ai5e6m49w\",\"name\":\"블록 이름\"},\"userRequest\":{\"timezone\":\"Asia/Seoul\",\"params\":{\"ignoreMe\":\"true\"},\"block\":{\"id\":\"viqkzodj54puyo8ai5e6m49w\",\"name\":\"블록 이름\"},\"utterance\":\"발화 내용\",\"lang\":null,\"user\":{\"id\":\"702912\",\"type\":\"accountId\",\"properties\":{}}},\"bot\":{\"id\":\"5e0f180affa74800014bd33d\",\"name\":\"봇 이름\"},\"action\":{\"name\":\"u0o68ejcea\",\"clientExtra\":null,\"params\":{\"meal\":\"view\"},\"id\":\"lwnhtra9gidojk68b6xr41o4\",\"detailParams\":{\"meal\":{\"origin\":\"view\",\"value\":\"view\",\"groupName\":\"\"}}}}", + "body": "{'bot': {'id': '5e0f180affa74800014bd33d!', 'name': '산돌이'}, 'intent': {'id': '660dfdbc8690b74ed8013553', 'name': '학식 보기_', 'extra': {'reason': {'code': 1, 'message': 'OK'}}}, 'action': {'id': '660df94d9393ef1662327b74', 'name': '학식 보기_ㅌ', 'params': {'CollegeCaF': '학식', 'TestNTT': '테스트'}, 'detailParams': {'CollegeCaF': {'groupName': '', 'origin': '학식', 'value': '학식'}, 'TestNTT': {'groupName': '', 'origin': '테스트', 'value': '테스트'}}, 'clientExtra': {}}, 'userRequest': {'block': {'id': '660dfdbc8690b74ed8013553', 'name': '학식 보기_'}, 'user': {'id': '00752cd364a4d1c910df203fd4ed424e07e6981d3f958f953c6c5ef3b879032743', 'type': 'botUserKey', 'properties': {'botUserKey': '00752cd364a4d1c910df203fd4ed424e07e6981d3f958f953c6c5ef3b879032743', 'bot_user_key': '00752cd364a4d1c910df203fd4ed424e07e6981d3f958f953c6c5ef3b879032743'}}, 'utterance': '테스트 학식', 'params': {'ignoreMe': 'true', 'surface': 'BuilderBotTest'}, 'lang': 'ko', 'timezone': 'Asia/Seoul'}, 'contexts': []}", "isBase64Encoded": false } diff --git a/packaged.yaml b/packaged.yaml index 89c2feee..ccf6234a 100644 --- a/packaged.yaml +++ b/packaged.yaml @@ -6,7 +6,7 @@ Resources: Properties: Handler: app.handler Runtime: python3.11 - CodeUri: s3://sandol-deploy-bucket/773a22c99daca4bf37a9bcfe7c23d54f + CodeUri: s3://sandol-deploy-bucket/60847926f3d6e4910d3d5df22f072833 MemorySize: 128 Timeout: 300 Policies: diff --git a/sandol/api_server/meal.py b/sandol/api_server/meal.py index 7cdb1c43..03c60e0b 100644 --- a/sandol/api_server/meal.py +++ b/sandol/api_server/meal.py @@ -5,6 +5,7 @@ """ from datetime import datetime, timedelta +from typing import List from fastapi import Request from fastapi import APIRouter @@ -215,19 +216,21 @@ async def meal_view(request: Request): cafeteria_list: list[Restaurant] = get_meals() # cafeteria 값이 있을 경우 해당 식당 정보로 필터링 + + target_cafeteria = False + + cafeteria_list: List[Restaurant] = await get_meals() + if target_cafeteria: - restaurants = list( - filter(lambda x: x.name == target_cafeteria, cafeteria_list)) + restaurants = list(filter(lambda x: x.name == target_cafeteria, cafeteria_list)) else: restaurants = cafeteria_list - # 어제 7시를 기준으로 식당 정보를 필터링 standard_time = datetime.now() - timedelta(days=1) - standard_time = standard_time.replace( - hour=19, minute=0, second=0, microsecond=0) + standard_time = standard_time.replace(hour=19, minute=0, second=0, microsecond=0) - af_standard: list[Restaurant] = [] - bf_standard: list[Restaurant] = [] + af_standard: List[Restaurant] = [] + bf_standard: List[Restaurant] = [] for r in restaurants: if r.registration_time < standard_time: bf_standard.append(r) @@ -237,39 +240,30 @@ async def meal_view(request: Request): bf_standard.sort(key=lambda x: x.registration_time) af_standard.sort(key=lambda x: x.registration_time) - # 어제 7시 이후 등록된 식당 정보를 먼저 배치 restaurants = af_standard + bf_standard - # 점심과 저녁 메뉴를 담은 Carousel 생성 lunch_carousel, dinner_carousel = make_meal_cards(restaurants) response = KakaoResponse() - # 점심과 저녁 메뉴 Carousel을 SkillList에 추가 - # 비어있는 Carousel을 추가하지 않음 - if not lunch_carousel.is_empty: + if lunch_carousel: response.add_component(lunch_carousel) - if not dinner_carousel.is_empty: + if dinner_carousel: response.add_component(dinner_carousel) if response.is_empty: - response.add_component( - SimpleTextComponent("식단 정보가 없습니다.") - ) + response.add_component(SimpleTextComponent("식단 정보가 없습니다.")) - # 퀵리플라이 추가 - # 현재 선택된 식단을 제외한 다른 식당을 퀵리플라이로 추가 if target_cafeteria: response.add_quick_reply( label="모두 보기", action="message", - message_text="테스트 학식", # TODO(Seokyoung_Hong): 배포 시 '테스트' 제거 + message_text="테스트 학식", ) for rest in cafeteria_list: if rest.name != target_cafeteria: response.add_quick_reply( label=rest.name, action="message", - # TODO(Seokyoung_Hong): 배포 시 '테스트' 제거 message_text=f"테스트 학식 {rest.name}", ) diff --git a/sandol/api_server/utils.py b/sandol/api_server/utils.py index bed12c02..9848d7fe 100644 --- a/sandol/api_server/utils.py +++ b/sandol/api_server/utils.py @@ -210,12 +210,8 @@ def check_tip_and_e(func): """ @wraps(func) async def wrapper(*args, **kwargs): - # 파일의 수정 시간 확인 - file_path = os.path.join( - os.path.dirname(os.path.dirname(__file__)), - "crawler", - "data.xlsx" - ) + # Lambda 환경에서 /tmp 디렉토리를 사용 + file_path = "/tmp/data.xlsx" must_download = False if os.path.exists(file_path): @@ -231,15 +227,15 @@ async def wrapper(*args, **kwargs): hour=0, minute=0, second=0, microsecond=0) # 파일 수정 시간이 이번 주 일요일 이후인지 확인 - if must_download or not file_mod_datetime > start_of_week: - downloader = BookDownloader() - downloader.get_file(file_path) # data.xlsx에 파일 저장 - - ibook = BookTranslator() - - # 식단 정보 test.json에 저장 - ibook.submit_tip_info() - ibook.submit_e_info() + # if must_download or not file_mod_datetime > start_of_week: + # downloader = BookDownloader() + # downloader.get_file(file_path) # /tmp/data.xlsx에 파일 저장 + # + # ibook = BookTranslator() + # + # # 식단 정보 test.json에 저장 + # ibook.submit_tip_info() + # ibook.submit_e_info() return await func(*args, **kwargs) return wrapper diff --git a/sandol/bucket/__init__.py b/sandol/bucket/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sandol/bucket/common.py b/sandol/bucket/common.py new file mode 100644 index 00000000..dca29b4b --- /dev/null +++ b/sandol/bucket/common.py @@ -0,0 +1,30 @@ +import boto3 +from botocore.exceptions import NoCredentialsError, PartialCredentialsError + +BUCKET_NAME = "sandol-bucket" +FILE_KEY = "test.json" + + +def get_s3_client(): + return boto3.client('s3') + + +def download_file_from_s3(bucket_name, file_key, download_path): + s3 = get_s3_client() + try: + s3.download_file(bucket_name, file_key, download_path) + except (NoCredentialsError, PartialCredentialsError): + print("AWS 자격 증명이 필요합니다.") + raise + except s3.exceptions.NoSuchKey: + print(f"{file_key} 파일을 찾을 수 없습니다.") + raise FileNotFoundError(f"{file_key} 파일을 찾을 수 없습니다.") + + +def upload_file_to_s3(file_path, bucket_name, file_key): + s3 = get_s3_client() + try: + s3.upload_file(file_path, bucket_name, file_key) + except (NoCredentialsError, PartialCredentialsError): + print("AWS 자격 증명이 필요합니다.") + raise diff --git a/sandol/bucket/meal_bucket.py b/sandol/bucket/meal_bucket.py new file mode 100644 index 00000000..a6574e55 --- /dev/null +++ b/sandol/bucket/meal_bucket.py @@ -0,0 +1,12 @@ +# get_meals.py + +import os +import json +from common import BUCKET_NAME, FILE_KEY, download_file_from_s3 + + +class Restaurant: + @staticmethod + def by_dict(data): + # 데이터 처리 로직 (예시) + return data diff --git a/sandol/crawler/cafeteria.py b/sandol/crawler/cafeteria.py index 653f3f40..b224a4ae 100644 --- a/sandol/crawler/cafeteria.py +++ b/sandol/crawler/cafeteria.py @@ -1,6 +1,8 @@ import os import json import datetime as dt + +from bucket.common import download_file_from_s3, BUCKET_NAME, FILE_KEY from . import settings @@ -219,12 +221,12 @@ def submit(self): restaurant_found = False for restaurant_data in data: - if restaurant_data["name"] == self.name: # 식당 검색 - restaurant_data["lunch_menu"] = self.submit_update_menu("lunch") # 점심 메뉴 변경 사항 존재 시 submit + if restaurant_data["name"] == self.name: # 식당 검색 + restaurant_data["lunch_menu"] = self.submit_update_menu("lunch") # 점심 메뉴 변경 사항 존재 시 submit restaurant_data["dinner_menu"] = self.submit_update_menu("dinner") # 저녁 메뉴 변경 사항 존재 시 submit - restaurant_data["registration_time"] = dt.datetime.now().isoformat() # registration time update - restaurant_data["opening_time"] = self.opening_time # opining time update - restaurant_data["price_per_person"] = self.price_per_person # 가격 update + restaurant_data["registration_time"] = dt.datetime.now().isoformat() # registration time update + restaurant_data["opening_time"] = self.opening_time # opining time update + restaurant_data["price_per_person"] = self.price_per_person # 가격 update restaurant_found = True break @@ -232,7 +234,7 @@ def submit(self): raise ValueError(f"레스토랑 '{self.name}'가 test.json 파일에 존재하지 않습니다.") with open(filename, 'w', encoding='utf-8') as file: - json.dump(data, file, ensure_ascii=False, indent=4) # json data 한꺼번에 test.json으로 덮어씌우기 + json.dump(data, file, ensure_ascii=False, indent=4) # json data 한꺼번에 test.json으로 덮어씌우기 # 임시 파일 삭제 temp_menu_path = os.path.join(current_dir, f'{self.name}_temp_menu.json') @@ -251,23 +253,23 @@ def __str__(self): f"Opening_time: {self.opening_time}, Price: {self.price_per_person}") -def get_meals() -> list: - current_dir = os.path.dirname(__file__) - filename = os.path.join(current_dir, 'test.json') +async def get_meals() -> list: + download_path = '/tmp/test.json' # 임시 경로에 파일 다운로드 + + download_file_from_s3(BUCKET_NAME, FILE_KEY, download_path) - with open(filename, 'r', encoding='utf-8') as file: + with open(download_path, 'r', encoding='utf-8') as file: data = json.load(file) # 식당 목록 리스트 restaurants = [Restaurant.by_dict(item) for item in data] - return restaurants if __name__ == "__main__": # rests = get_meals() # print(rests) - identification = "001" # 001: TIP 가가식당 + identification = "001" # 001: TIP 가가식당 rest = Restaurant.by_id(identification) print(rest) diff --git a/sandol/crawler/ibookcrawler.py b/sandol/crawler/ibookcrawler.py index c7fed876..cbb83612 100644 --- a/sandol/crawler/ibookcrawler.py +++ b/sandol/crawler/ibookcrawler.py @@ -4,6 +4,8 @@ import os import math +from bucket.common import download_file_from_s3, BUCKET_NAME, FILE_KEY, upload_file_to_s3 + class BookTranslator: def __init__(self): @@ -19,18 +21,17 @@ def __init__(self): self.location = "" self.price_per_person = 0 + # Lambda 환경에서는 /tmp 디렉토리를 사용 current_dir = os.path.dirname(__file__) - filename = os.path.join(current_dir, 'data.xlsx') + filename = os.path.join('/tmp', 'data.xlsx') self.df = pd.read_excel(filename) - # print(self.df) - def save_menu(self, restaurant): """ - 당일 요일 검색 - TIP 식당 메뉴 저장 메서드 tip_save_menu() 호출 - E동 식당 메뉴 저장 메서드 e_save_menu() 호출 + 당일 요일 검색 + TIP 식당 메뉴 저장 메서드 tip_save_menu() 호출 + E동 식당 메뉴 저장 메서드 e_save_menu() 호출 """ now = dt.datetime.now() # current time search weekday = now.isoweekday() # 요일 구분 @@ -44,32 +45,32 @@ def save_menu(self, restaurant): def tip_save_menu(self, weekday): """ - TIP 가가식당 메뉴 저장 - today_menu() 메서드에서 요일정보 획득 - data.xlsx 파일에서 요일에 해당하는 점심메뉴, 저녁메뉴 추출 + TIP 가가식당 메뉴 저장 + today_menu() 메서드에서 요일정보 획득 + data.xlsx 파일에서 요일에 해당하는 점심메뉴, 저녁메뉴 추출 """ - self.tip_lunch_menu = list(self.df.iloc[6:12, weekday]) # data.xlsx file 내 1열 8행~13행 + self.tip_lunch_menu = list(self.df.iloc[6:12, weekday]) # data.xlsx file 내 1열 8행~13행 for menu in self.tip_lunch_menu: if menu == '*복수메뉴*': self.tip_lunch_menu.remove(menu) - self.tip_dinner_menu = list(self.df.iloc[13:19, weekday]) # data.xlsx file 내 1열 15행~20행 + self.tip_dinner_menu = list(self.df.iloc[13:19, weekday]) # data.xlsx file 내 1열 15행~20행 for menu in self.tip_lunch_menu: if menu == '*복수메뉴*': self.tip_lunch_menu.remove(menu) def e_save_menu(self, weekday): """ - E동 메뉴 저장 - today_menu() 메서드에서 요일정보 획득 - data.xlsx 파일에서 요일에 해당하는 점심메뉴, 저녁메뉴 추출 + E동 메뉴 저장 + today_menu() 메서드에서 요일정보 획득 + data.xlsx 파일에서 요일에 해당하는 점심메뉴, 저녁메뉴 추출 """ self.e_lunch_menu = list(self.df.iloc[22:29, weekday]) # data.xlsx file 내 1열 24행~30행 self.e_dinner_menu = list(self.df.iloc[30:37, weekday]) # data.xlsx file 내 1열 32행~38행 def save_tip_info(self): - self.save_menu("TIP") # restaurant = "TIP" -> tip_save_menu() + self.save_menu("TIP") # restaurant = "TIP" -> tip_save_menu() self.identification = "001" self.name = "TIP 가가식당" self.opening_time = "오전 11시-2시 / 오후 5시-6:50" @@ -77,7 +78,7 @@ def save_tip_info(self): self.price_per_person = 6000 def save_e_info(self): - self.save_menu("E") # restaurant = "E" -> e_save_menu() + self.save_menu("E") # restaurant = "E" -> e_save_menu() self.identification = "002" self.name = "E동 레스토랑" self.opening_time = "오전 11:30-13:50 / 오후 4:50-18:40" @@ -86,41 +87,31 @@ def save_e_info(self): def submit_tip_info(self): """ - save_tip_info 로 팁지 정보 저장 - tip_info {} 로 json 파일 형식으로 저장 - test.json 파일에 덮어쓰기 -> 이미 식당 정보가 존재한다면 lunch, dinner 메뉴만 덮어쓰기 - NaN(엑셀파일 기준 빈칸) 값 제거 후 test.json 저장 + save_tip_info 로 팁지 정보 저장 + tip_info {} 로 json 파일 형식으로 저장 + test.json 파일에 덮어쓰기 -> 이미 식당 정보가 존재한다면 lunch, dinner 메뉴만 덮어쓰기 + NaN(엑셀파일 기준 빈칸) 값 제거 후 test.json 저장 """ self.save_tip_info() - self.tip_info = { - "identification": self.identification, - "name": self.name, - "registration_time": dt.datetime.now().isoformat(), - "opening_time": self.opening_time, - "lunch_menu": self.tip_lunch_menu, - "dinner_menu": self.tip_dinner_menu, - "location": self.location, - "price_per_person": self.price_per_person - } - - current_dir = os.path.dirname(__file__) - filename = os.path.join(current_dir, 'test.json') - - # read and write + # S3에서 파일 다운로드 + download_path = '/tmp/test.json' try: - with open(filename, 'r', encoding='utf-8') as file: + download_file_from_s3(BUCKET_NAME, FILE_KEY, download_path) + with open(download_path, 'r', encoding='utf-8') as file: data = json.load(file) + if not isinstance(data, list): + data = [] + except FileNotFoundError: + data = [] except json.decoder.JSONDecodeError: data = [] - except FileNotFoundError: - raise FileNotFoundError(f"{filename} 파일을 찾을 수 없습니다.") restaurant_found = False for restaurant_data in data: - if restaurant_data["name"] == self.name: # 식당 검색 - restaurant_data["lunch_menu"] = self.tip_lunch_menu - restaurant_data["dinner_menu"] = self.tip_dinner_menu + if restaurant_data.get("name") == self.tip_info["name"]: # 식당 검색 + restaurant_data["lunch_menu"] = self.tip_info["lunch_menu"] + restaurant_data["dinner_menu"] = self.tip_info["dinner_menu"] restaurant_found = True break @@ -132,15 +123,20 @@ def submit_tip_info(self): if isinstance(value, list): restaurant[key] = [item for item in value if not (isinstance(item, float) and math.isnan(item))] - with open(filename, 'w', encoding='utf-8') as file: + # 임시 파일에 데이터 저장 + with open(download_path, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) + # S3에 업로드 + upload_file_to_s3(download_path, BUCKET_NAME, FILE_KEY) + print(f"File {FILE_KEY} uploaded to S3 bucket {BUCKET_NAME}") + def submit_e_info(self): """ - save_e_info 로 E동 정보 저장 - e_info {} 로 json 파일 형식으로 저장 - test.json 파일에 덮어쓰기 -> 이미 식당 정보가 존재한다면 lunch, dinner 메뉴만 덮어쓰기 - NaN(엑셀파일 기준 빈칸) 값 제거 후 test.json 저장 + save_e_info 로 E동 정보 저장 + e_info {} 로 json 파일 형식으로 저장 + test.json 파일에 덮어쓰기 -> 이미 식당 정보가 존재한다면 lunch, dinner 메뉴만 덮어쓰기 + NaN(엑셀파일 기준 빈칸) 값 제거 후 test.json 저장 """ self.save_e_info() @@ -190,5 +186,5 @@ def submit_e_info(self): if __name__ == "__main__": ibook = BookTranslator() - ibook.submit_tip_info() # TIP 가가식당 정보 test.json에 저장 - ibook.submit_e_info() # E동 레스토랑 정보 test.json에 저장 + ibook.submit_tip_info() # TIP 가가식당 정보 test.json에 저장 + ibook.submit_e_info() # E동 레스토랑 정보 test.json에 저장 diff --git a/sandol/crawler/ibookdownloader.py b/sandol/crawler/ibookdownloader.py index a63bdaff..5039a590 100644 --- a/sandol/crawler/ibookdownloader.py +++ b/sandol/crawler/ibookdownloader.py @@ -8,6 +8,7 @@ downloader = BookDownloader(link) # BookDownloader 객체 생성 downloader.get_file("data.xlsx") # data.xlsx에 파일 저장 """ +import os from typing import Optional from xml.etree import ElementTree import requests @@ -194,10 +195,11 @@ def download_file(self, file_url, save_as): # 파일 다운로드 성공 if response.status_code == 200: - # 파일 저장 - with open(save_as, "wb") as f: + # Lambda의 /tmp 디렉토리에 파일 저장 + save_path = os.path.join('/tmp', save_as) + with open(save_path, "wb") as f: f.write(response.content) - + print(f"File saved to {save_path}") else: # 파일 다운로드 실패 raise DownloadFileError(response.status_code) diff --git a/sandol/crawler/test.json b/sandol/crawler/test.json index 0115042b..2a1796da 100644 --- a/sandol/crawler/test.json +++ b/sandol/crawler/test.json @@ -116,5 +116,21 @@ ], "location": "E동 1층", "price_per_person": 6500 + }, + { + "identification": "1234", + "name": "Sample Restaurant", + "registration_time": "2024-06-03T13:33:05.326819", + "opening_time": "09:00", + "lunch_menu": [ + "lunch item 1", + "lunch item 2" + ], + "dinner_menu": [ + "dinner item 1", + "dinner item 2" + ], + "location": "Sample Location", + "price_per_person": 20.0 } -] \ No newline at end of file +]