From 74478dab47053f441412ded6d39c13e7a85636df Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 08:32:48 +0000 Subject: [PATCH 001/187] Setting up GitHub Classroom Feedback From 8e48c26a2efbf96741f3439c041f502f814cb8c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Thu, 29 Feb 2024 08:02:20 +0900 Subject: [PATCH 002/187] Create PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..be51e04 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +## Overview +- + +## Change Log +- + +## To Reviewer +- + +## Issue Tags +- Closed | Fixed: # +- See also: # From fb4385e8b7f177711b1b64caf2bfcd5974e18db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Thu, 29 Feb 2024 08:02:52 +0900 Subject: [PATCH 003/187] Create feature_request.md --- .github/ISSUE_TEMPLATE /feature_request.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE /feature_request.md diff --git a/.github/ISSUE_TEMPLATE /feature_request.md b/.github/ISSUE_TEMPLATE /feature_request.md new file mode 100644 index 0000000..8aff911 --- /dev/null +++ b/.github/ISSUE_TEMPLATE /feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: 새로운 기능을 추가할 때 사용하는 템플릿 +title: "[FEAT] " +labels: '' +assignees: '' + +--- + +## Background +- + +## To do +- [ ] From de5e98f78c13a72add8fd4b534747bd13e793d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Thu, 29 Feb 2024 08:03:12 +0900 Subject: [PATCH 004/187] Create bug_report.md --- .github/ISSUE_TEMPLATE /bug_report.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE /bug_report.md diff --git a/.github/ISSUE_TEMPLATE /bug_report.md b/.github/ISSUE_TEMPLATE /bug_report.md new file mode 100644 index 0000000..7429a72 --- /dev/null +++ b/.github/ISSUE_TEMPLATE /bug_report.md @@ -0,0 +1,18 @@ +--- +name: Bug report +about: 버그 발생 시 사용하는 템플릿 +title: "[BUG] " +labels: '' +assignees: '' + +--- + +## Description + +## How to reproduce + +1. +2. +3. + +## Solution From 264c8bf0715668787002776e4d1090f2cc9a587d Mon Sep 17 00:00:00 2001 From: twndus Date: Fri, 1 Mar 2024 17:21:53 +0900 Subject: [PATCH 005/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80,=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=EB=A0=89=EC=85=98=20=ED=81=AC=EB=A1=A4=EB=A7=81=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=97=85=EB=A1=9C=EB=93=9C=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_recipe.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crawl_recipe.py diff --git a/crawl_recipe.py b/crawl_recipe.py new file mode 100644 index 0000000..b7bb123 --- /dev/null +++ b/crawl_recipe.py @@ -0,0 +1,100 @@ +import re + +import pandas as pd +from tqdm import tqdm + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from webdriver_manager.chrome import ChromeDriverManager + +from bs4 import BeautifulSoup + +def get_userid_set(): + # filenames + RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' + RECIPE_FILE2 = 'TB_RECIPE_SEARCH-20231130.csv' + + # read file + recipe_df_22 = pd.read_csv(RECIPE_FILE1, engine='python', encoding='cp949', encoding_errors='ignore') # EUC-KR, utf-8, cp949, ms949, iso2022_jp_2, iso2022_kr johab + recipe_df_23 = pd.read_csv(RECIPE_FILE2, engine='python', encoding='cp949', encoding_errors='ignore') + + # union users + userset_22 = set(recipe_df_22['RGTR_ID'].values) + userset_23 = set(recipe_df_23['RGTR_ID'].values) + userset_all = userset_22 | userset_23 + + print(len(userset_22), len(userset_23), len(userset_all)) + return userset_all + +def get_html_source(driver, uid:str=16221801): + + url = f'https://m.10000recipe.com/profile/review.html?uid={uid}' + driver.get(url) # url 접속 + driver.implicitly_wait(2) + + # 페이지의 HTML 소스 가져오기 + return driver.page_source + +def parse_recipe_id(review): + recipe_id = '' + onclick_attr = review.get('onclick') + if onclick_attr: + match = re.search(r"location.href='([^']+)'", onclick_attr) + if match: + # URL 출력 + url = match.group(1) + recipe_id = url.split('/')[-1] + return recipe_id + +def save_results(data_list): + + # build df + df = pd.DataFrame(data_list) + # save + df.to_csv('results.csv') + + +def main(): + # get all user ids + userid_set = get_userid_set() + + # get automative driver + driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) + + # datalist + data_list = [] + + # collect data by user id + for i,uid in enumerate(tqdm(userid_set)): + + try: + html_source = get_html_source(driver, uid) # temporarily fixed + soup = BeautifulSoup(html_source, 'html.parser') + + nickname = soup.find('p', 'pic_r_name').text.split('\n')[0].strip() + #print(nickname) + + # parse review by recipes + user_history = dict() + for review in soup.find('ul', id='listDiv').find_all('div', 'media'): + recipe_id = parse_recipe_id(review) + if len(recipe_id) <= 0: continue + rating = len(review.find('span', 'view2_review_star').find_all('img')) + user_history[recipe_id] = rating + + if len(user_history) > 0: + data_list.append({ + 'uid': uid, + 'user_name': nickname, + 'history': user_history, + }) + except: + continue +# finally: +# if len(data_list) > 10: break + + # save results + save_results(data_list) + +if __name__ == '__main__': + main() From 4156fc4cd69b6cae78c2248ca49ce56dab558fd8 Mon Sep 17 00:00:00 2001 From: twndus Date: Fri, 1 Mar 2024 19:31:34 +0900 Subject: [PATCH 006/187] =?UTF-8?q?feat:=20=EB=8D=94=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=84=20=EB=88=8C=EB=9F=AC=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=ED=9B=84=EA=B8=B0=EB=A5=BC=20=EC=88=98=EC=A7=91?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=B4=EC=99=84=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_recipe.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crawl_recipe.py b/crawl_recipe.py index b7bb123..4af1fb8 100644 --- a/crawl_recipe.py +++ b/crawl_recipe.py @@ -6,6 +6,7 @@ from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By from bs4 import BeautifulSoup @@ -32,6 +33,14 @@ def get_html_source(driver, uid:str=16221801): driver.get(url) # url 접속 driver.implicitly_wait(2) + # 후기 수// 10 만큼 더보기 버튼 누르기 + num_review = int(driver.find_element(By.CLASS_NAME, 'myhome_cont').find_element(By.CLASS_NAME, 'active').find_element(By.CLASS_NAME, 'num').text) + + for i in range(num_review//10): + btn_href = driver.find_elements(By.CLASS_NAME, 'view_btn_more')[-1].find_element(By.TAG_NAME, 'a') + driver.execute_script("arguments[0].click();", btn_href) #자바 명령어 실행 + driver.implicitly_wait(2) + # 페이지의 HTML 소스 가져오기 return driver.page_source @@ -72,7 +81,6 @@ def main(): soup = BeautifulSoup(html_source, 'html.parser') nickname = soup.find('p', 'pic_r_name').text.split('\n')[0].strip() - #print(nickname) # parse review by recipes user_history = dict() @@ -90,8 +98,6 @@ def main(): }) except: continue -# finally: -# if len(data_list) > 10: break # save results save_results(data_list) From e3a1a26b5be652952b6a22b4970353dc5256b89c Mon Sep 17 00:00:00 2001 From: Gangbean Date: Fri, 1 Mar 2024 23:08:53 +0900 Subject: [PATCH 007/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_recipe.py | 92 ++++++++++++++++++++--------------------- crawl_review.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 46 deletions(-) create mode 100644 crawl_review.py diff --git a/crawl_recipe.py b/crawl_recipe.py index 4af1fb8..175184a 100644 --- a/crawl_recipe.py +++ b/crawl_recipe.py @@ -1,4 +1,4 @@ -import re +import re, os import pandas as pd from tqdm import tqdm @@ -7,6 +7,7 @@ from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By +from selenium.common.exceptions import UnexpectedAlertPresentException from bs4 import BeautifulSoup @@ -19,30 +20,29 @@ def get_userid_set(): recipe_df_22 = pd.read_csv(RECIPE_FILE1, engine='python', encoding='cp949', encoding_errors='ignore') # EUC-KR, utf-8, cp949, ms949, iso2022_jp_2, iso2022_kr johab recipe_df_23 = pd.read_csv(RECIPE_FILE2, engine='python', encoding='cp949', encoding_errors='ignore') - # union users - userset_22 = set(recipe_df_22['RGTR_ID'].values) - userset_23 = set(recipe_df_23['RGTR_ID'].values) - userset_all = userset_22 | userset_23 + # union recipes + recipeset_22 = set(recipe_df_22['RGTR_ID'].values) + recipeset_23 = set(recipe_df_23['RGTR_ID'].values) + recipeset_all = recipeset_22 | recipeset_23 - print(len(userset_22), len(userset_23), len(userset_all)) - return userset_all + print(len(recipeset_22), len(recipeset_23), len(recipeset_all)) + return recipeset_all -def get_html_source(driver, uid:str=16221801): - - url = f'https://m.10000recipe.com/profile/review.html?uid={uid}' +def get_html_source(driver, uid:str='pingky7080', page_no=1): + url = f'https://m.10000recipe.com/profile/recipe.html?uid={uid}&page={page_no}' driver.get(url) # url 접속 - driver.implicitly_wait(2) + driver.implicitly_wait(3) - # 후기 수// 10 만큼 더보기 버튼 누르기 - num_review = int(driver.find_element(By.CLASS_NAME, 'myhome_cont').find_element(By.CLASS_NAME, 'active').find_element(By.CLASS_NAME, 'num').text) + return driver.page_source - for i in range(num_review//10): - btn_href = driver.find_elements(By.CLASS_NAME, 'view_btn_more')[-1].find_element(By.TAG_NAME, 'a') - driver.execute_script("arguments[0].click();", btn_href) #자바 명령어 실행 - driver.implicitly_wait(2) +def parse_user_recipes(soup): + user_recipes = list() + for recipe in soup.find('div', 'recipe_list').find_all('div', 'media'): + recipe_id = parse_recipe_id(recipe) + if len(recipe_id) <= 0: continue + user_recipes.append(recipe_id) - # 페이지의 HTML 소스 가져오기 - return driver.page_source + return user_recipes def parse_recipe_id(review): recipe_id = '' @@ -59,48 +59,48 @@ def save_results(data_list): # build df df = pd.DataFrame(data_list) - # save - df.to_csv('results.csv') + + PATH = 'results.csv' + if os.path.exists(PATH): + # save + df.to_csv('results.csv', mode='a', index=False, header=False) + else: + df.to_csv('results.csv', index=False) def main(): # get all user ids - userid_set = get_userid_set() + recipeid_set = get_userid_set() # get automative driver driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) - # datalist - data_list = [] - # collect data by user id - for i,uid in enumerate(tqdm(userid_set)): - + for i,uid in enumerate(tqdm(recipeid_set)): try: - html_source = get_html_source(driver, uid) # temporarily fixed + html_source = get_html_source(driver, uid) soup = BeautifulSoup(html_source, 'html.parser') + num_recipe = int(soup.find('div', 'myhome_cont').find('li', 'active').find('p', 'num').text) + + next_page_num: int = num_recipe // 20 + user_recipes = list() + user_recipes.extend(parse_user_recipes(soup)) - nickname = soup.find('p', 'pic_r_name').text.split('\n')[0].strip() + for page_no in range(2, next_page_num+2): + # parsing + next_page_source = get_html_source(driver, uid, page_no) + soup = BeautifulSoup(next_page_source, 'html.parser') - # parse review by recipes - user_history = dict() - for review in soup.find('ul', id='listDiv').find_all('div', 'media'): - recipe_id = parse_recipe_id(review) - if len(recipe_id) <= 0: continue - rating = len(review.find('span', 'view2_review_star').find_all('img')) - user_history[recipe_id] = rating - - if len(user_history) > 0: - data_list.append({ + # parse review by recipes + user_recipes.extend(parse_user_recipes(soup)) + + if len(user_recipes) > 0: + save_results([{ 'uid': uid, - 'user_name': nickname, - 'history': user_history, - }) - except: + 'recipes': user_recipes, + }]) + except UnexpectedAlertPresentException: continue - # save results - save_results(data_list) - if __name__ == '__main__': main() diff --git a/crawl_review.py b/crawl_review.py new file mode 100644 index 0000000..4af1fb8 --- /dev/null +++ b/crawl_review.py @@ -0,0 +1,106 @@ +import re + +import pandas as pd +from tqdm import tqdm + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By + +from bs4 import BeautifulSoup + +def get_userid_set(): + # filenames + RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' + RECIPE_FILE2 = 'TB_RECIPE_SEARCH-20231130.csv' + + # read file + recipe_df_22 = pd.read_csv(RECIPE_FILE1, engine='python', encoding='cp949', encoding_errors='ignore') # EUC-KR, utf-8, cp949, ms949, iso2022_jp_2, iso2022_kr johab + recipe_df_23 = pd.read_csv(RECIPE_FILE2, engine='python', encoding='cp949', encoding_errors='ignore') + + # union users + userset_22 = set(recipe_df_22['RGTR_ID'].values) + userset_23 = set(recipe_df_23['RGTR_ID'].values) + userset_all = userset_22 | userset_23 + + print(len(userset_22), len(userset_23), len(userset_all)) + return userset_all + +def get_html_source(driver, uid:str=16221801): + + url = f'https://m.10000recipe.com/profile/review.html?uid={uid}' + driver.get(url) # url 접속 + driver.implicitly_wait(2) + + # 후기 수// 10 만큼 더보기 버튼 누르기 + num_review = int(driver.find_element(By.CLASS_NAME, 'myhome_cont').find_element(By.CLASS_NAME, 'active').find_element(By.CLASS_NAME, 'num').text) + + for i in range(num_review//10): + btn_href = driver.find_elements(By.CLASS_NAME, 'view_btn_more')[-1].find_element(By.TAG_NAME, 'a') + driver.execute_script("arguments[0].click();", btn_href) #자바 명령어 실행 + driver.implicitly_wait(2) + + # 페이지의 HTML 소스 가져오기 + return driver.page_source + +def parse_recipe_id(review): + recipe_id = '' + onclick_attr = review.get('onclick') + if onclick_attr: + match = re.search(r"location.href='([^']+)'", onclick_attr) + if match: + # URL 출력 + url = match.group(1) + recipe_id = url.split('/')[-1] + return recipe_id + +def save_results(data_list): + + # build df + df = pd.DataFrame(data_list) + # save + df.to_csv('results.csv') + + +def main(): + # get all user ids + userid_set = get_userid_set() + + # get automative driver + driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) + + # datalist + data_list = [] + + # collect data by user id + for i,uid in enumerate(tqdm(userid_set)): + + try: + html_source = get_html_source(driver, uid) # temporarily fixed + soup = BeautifulSoup(html_source, 'html.parser') + + nickname = soup.find('p', 'pic_r_name').text.split('\n')[0].strip() + + # parse review by recipes + user_history = dict() + for review in soup.find('ul', id='listDiv').find_all('div', 'media'): + recipe_id = parse_recipe_id(review) + if len(recipe_id) <= 0: continue + rating = len(review.find('span', 'view2_review_star').find_all('img')) + user_history[recipe_id] = rating + + if len(user_history) > 0: + data_list.append({ + 'uid': uid, + 'user_name': nickname, + 'history': user_history, + }) + except: + continue + + # save results + save_results(data_list) + +if __name__ == '__main__': + main() From 661d9daee6e5da07d2a95e9a1955fa5203eba43a Mon Sep 17 00:00:00 2001 From: Gangbean Date: Sat, 2 Mar 2024 01:35:09 +0900 Subject: [PATCH 008/187] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=EC=96=B4=20=ED=81=AC=EB=A1=A4=EB=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_reviewer.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 crawl_reviewer.py diff --git a/crawl_reviewer.py b/crawl_reviewer.py new file mode 100644 index 0000000..1c8e9c6 --- /dev/null +++ b/crawl_reviewer.py @@ -0,0 +1,82 @@ +import os +from datetime import datetime as dt +from tqdm import tqdm +import pandas as pd + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.common.by import By +from webdriver_manager.chrome import ChromeDriverManager + +from bs4 import BeautifulSoup + +def get_recipesno_set(): + # filenames + RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' + RECIPE_FILE2 = 'TB_RECIPE_SEARCH-20231130.csv' + + # read file + recipe_df_22 = pd.read_csv(RECIPE_FILE1, engine='python', encoding='cp949', encoding_errors='ignore') # EUC-KR, utf-8, cp949, ms949, iso2022_jp_2, iso2022_kr johab + recipe_df_23 = pd.read_csv(RECIPE_FILE2, engine='python', encoding='cp949', encoding_errors='ignore') + + # union recipes + recipeset_22 = set(recipe_df_22['RCP_SNO'].values) + recipeset_23 = set(recipe_df_23['RCP_SNO'].values) + recipeset_all = recipeset_22 | recipeset_23 + + print(len(recipeset_22), len(recipeset_23), len(recipeset_all)) + return recipeset_all + +def get_html_source(driver, sno:str=1785098): + + url = f'https://www.10000recipe.com/recipe/{sno}' + driver.get(url) # url 접속 + driver.implicitly_wait(2) + + # 페이지의 HTML 소스 가져오기 + return driver.page_source + +def save_results(data_list): + + # build df + df = pd.DataFrame(data_list) + date = dt.now().strftime('%y%m%d') + + PATH = f'reviewers_{date}.csv' + if os.path.exists(PATH): + # save + df.to_csv(PATH, mode='a', index=False, header=False) + else: + df.to_csv(PATH, index=False) + +def main(): + # get all recipe snos + recipe_snos = get_recipesno_set() + # get automative driver + # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) + driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager(driver_version='122.0.6261.94').install())) + + # collect data by recipe snos + for i, rsno in enumerate(tqdm(recipe_snos)): + try: + html_source = get_html_source(driver, rsno) + soup = BeautifulSoup(html_source, 'html.parser') + + reviews = soup.find_all('div', 'view_reply st2') + if len(reviews) == 0: continue + reviews = reviews[1].find_all('div', 'media-left') + recipe_reviewers = list() + for review in reviews: + recipe_reviewers.append(review.find('a')['href'].split('=')[-1]) + + save_results([{ + 'recipe_sno': rsno, + 'reviewers': recipe_reviewers + }]) + except KeyboardInterrupt: + exit() + except: + continue + +if __name__ == '__main__': + main() From ed4d92a5b572c5c736c5bf75e1247735d102949e Mon Sep 17 00:00:00 2001 From: Gangbean Date: Sat, 2 Mar 2024 01:39:17 +0900 Subject: [PATCH 009/187] =?UTF-8?q?refactor:=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?,=20=EB=A6=AC=EB=B7=B0=20=ED=81=AC=EB=A1=A4=EB=A7=81=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EB=B2=84=EC=A0=80=EB=8B=9D=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_recipe.py | 8 +++++--- crawl_review.py | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crawl_recipe.py b/crawl_recipe.py index 175184a..a980cee 100644 --- a/crawl_recipe.py +++ b/crawl_recipe.py @@ -1,4 +1,5 @@ import re, os +from datetime import datetime as dt import pandas as pd from tqdm import tqdm @@ -59,13 +60,14 @@ def save_results(data_list): # build df df = pd.DataFrame(data_list) + date = dt.now().strftime('%y%m%d') - PATH = 'results.csv' + PATH = f'recipes_{date}.csv' if os.path.exists(PATH): # save - df.to_csv('results.csv', mode='a', index=False, header=False) + df.to_csv(PATH, mode='a', index=False, header=False) else: - df.to_csv('results.csv', index=False) + df.to_csv(PATH, index=False) def main(): diff --git a/crawl_review.py b/crawl_review.py index 4af1fb8..6f8643d 100644 --- a/crawl_review.py +++ b/crawl_review.py @@ -1,4 +1,5 @@ import re +from datetime import datetime as dt import pandas as pd from tqdm import tqdm @@ -59,8 +60,12 @@ def save_results(data_list): # build df df = pd.DataFrame(data_list) + date = dt.now().strftime('%y%m%d') + + PATH = f'reviews_{date}.csv' + # save - df.to_csv('results.csv') + df.to_csv(PATH) def main(): From e10ed8544b48e0c2903b609cb041128ab4322674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:29:22 +0900 Subject: [PATCH 010/187] =?UTF-8?q?[Fix]=20=EC=9D=B4=EC=8A=88=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/{ISSUE_TEMPLATE => ISSUE_TEMPLATE}/feature_request.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ISSUE_TEMPLATE => ISSUE_TEMPLATE}/feature_request.md (100%) diff --git a/.github/ISSUE_TEMPLATE /feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md similarity index 100% rename from .github/ISSUE_TEMPLATE /feature_request.md rename to .github/ISSUE_TEMPLATE/feature_request.md From c978a2c6822d83c03c604fb9c150081a04bec985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Sat, 2 Mar 2024 16:29:59 +0900 Subject: [PATCH 011/187] =?UTF-8?q?[Fix]=20=EC=9D=B4=EC=8A=88=20=ED=85=9C?= =?UTF-8?q?=ED=94=8C=EB=A6=BF=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD=5FBug=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/{ISSUE_TEMPLATE => ISSUE_TEMPLATE}/bug_report.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ISSUE_TEMPLATE => ISSUE_TEMPLATE}/bug_report.md (100%) diff --git a/.github/ISSUE_TEMPLATE /bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md similarity index 100% rename from .github/ISSUE_TEMPLATE /bug_report.md rename to .github/ISSUE_TEMPLATE/bug_report.md From 864c1e356e15423827d8165ec6e7fec4097f4d45 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 2 Mar 2024 20:48:31 +0900 Subject: [PATCH 012/187] =?UTF-8?q?feat:=20CLI=EB=A1=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EA=B0=80=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_recipe.py | 6 +++++- crawl_review.py | 8 ++++++-- crawl_reviewer.py | 9 +++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crawl_recipe.py b/crawl_recipe.py index a980cee..13b86ca 100644 --- a/crawl_recipe.py +++ b/crawl_recipe.py @@ -74,8 +74,12 @@ def main(): # get all user ids recipeid_set = get_userid_set() + # set options for opening chrome browser in CLI env + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument('--headless') # headless 모드로 실행 + # get automative driver - driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) + driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) # collect data by user id for i,uid in enumerate(tqdm(recipeid_set)): diff --git a/crawl_review.py b/crawl_review.py index 6f8643d..d2f3954 100644 --- a/crawl_review.py +++ b/crawl_review.py @@ -65,15 +65,19 @@ def save_results(data_list): PATH = f'reviews_{date}.csv' # save - df.to_csv(PATH) + df.to_csv(PATH, index=False) def main(): # get all user ids userid_set = get_userid_set() + # set options for opening chrome browser in CLI env + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument('--headless') # headless 모드로 실행 + # get automative driver - driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) + driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) # datalist data_list = [] diff --git a/crawl_reviewer.py b/crawl_reviewer.py index 1c8e9c6..f3c799d 100644 --- a/crawl_reviewer.py +++ b/crawl_reviewer.py @@ -52,10 +52,15 @@ def save_results(data_list): def main(): # get all recipe snos recipe_snos = get_recipesno_set() + + # set options for opening chrome browser in CLI env + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument('--headless') # headless 모드로 실행 + # get automative driver # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) - driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager(driver_version='122.0.6261.94').install())) - + driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) + # collect data by recipe snos for i, rsno in enumerate(tqdm(recipe_snos)): try: From e35702ad5ae9d53e1f6b7fd3c069ddfa2af2d47b Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 2 Mar 2024 21:25:27 +0900 Subject: [PATCH 013/187] =?UTF-8?q?fix:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=EB=A7=81=20=EC=A0=84=EC=B2=B4=20=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=20=EC=88=98=20=ED=8C=8C=EC=8B=B1=EC=8B=9C=20?= =?UTF-8?q?Decimal=20Point=20=EC=A0=9C=EA=B1=B0=ED=95=98=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_recipe.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crawl_recipe.py b/crawl_recipe.py index 13b86ca..710dc7c 100644 --- a/crawl_recipe.py +++ b/crawl_recipe.py @@ -86,7 +86,11 @@ def main(): try: html_source = get_html_source(driver, uid) soup = BeautifulSoup(html_source, 'html.parser') - num_recipe = int(soup.find('div', 'myhome_cont').find('li', 'active').find('p', 'num').text) + + # total recipe count for pagination + num_recipe = soup.find('div', 'myhome_cont').find('li', 'active').find('p', 'num').text + num_recipe = re.sub(r'\D', '', num_recipe) # 숫자 아닌 값 제거; decimal point(,) 제거 + num_recipe = int(num_recipe) next_page_num: int = num_recipe // 20 user_recipes = list() From 3e85d9819aedf878acfed45022f6e07da160f2fc Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 2 Mar 2024 21:26:24 +0900 Subject: [PATCH 014/187] =?UTF-8?q?feat:=20=EB=AA=BD=EA=B3=A0DB=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mongo_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 mongo_test.py diff --git a/mongo_test.py b/mongo_test.py new file mode 100644 index 0000000..69d93e9 --- /dev/null +++ b/mongo_test.py @@ -0,0 +1,15 @@ +import pandas as pd +from pymongo import MongoClient + +# MongoDB 연결 설정 +client = MongoClient('mongodb://localhost:27017/') +db = client['database'] # 데이터베이스 선택 +collection = db['test'] # 컬렉션 선택 + +# CSV 파일 읽기 +PATH = 'reviews_240302.csv' +data = pd.read_csv(PATH) + +# MongoDB에 데이터 삽입 +records = data.to_dict(orient='records') +collection.insert_many(records) \ No newline at end of file From a003174f38edd213b9ad19fa0cd1cad146084b52 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sun, 3 Mar 2024 07:54:11 +0900 Subject: [PATCH 015/187] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=A9=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC=ED=98=84:?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C=20=EC=9E=85=EB=A0=A5=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_price.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 crawl_price.py diff --git a/crawl_price.py b/crawl_price.py new file mode 100644 index 0000000..7e88415 --- /dev/null +++ b/crawl_price.py @@ -0,0 +1,68 @@ +import re, os +from datetime import datetime as dt + +import pandas as pd +from tqdm import tqdm + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By +from selenium.common.exceptions import UnexpectedAlertPresentException + +from bs4 import BeautifulSoup + + +def get_html_source(driver, name): + url = f'https://alltimeprice.com/search/?search={name}' + driver.get(url) # url 접속 + driver.implicitly_wait(3) + + return driver.page_source + + +def save_results(data_list): + + # build df + df = pd.DataFrame(data_list) + date = dt.now().strftime('%y%m%d') + + PATH = f'prices_{date}.csv' + if os.path.exists(PATH): + # save + df.to_csv(PATH, mode='a', index=False, header=False) + else: + df.to_csv(PATH, index=False) + + +def main(): + + # set options for opening chrome browser in CLI env + chrome_options = webdriver.ChromeOptions() + chrome_options.add_argument('--headless') # headless 모드로 실행 + + # get automative driver + driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) + + # collect data by food name + food_set = set(['식빵', '봄동']) + for i, name in enumerate(tqdm(food_set)): + try: + html_source = get_html_source(driver, name) + soup = BeautifulSoup(html_source, 'html.parser') + + print(soup) + prices = soup.findAll('p', 'price') + print(prices) + if len(prices) > 0: + first_price = re.sub(r'\D', '', prices[0].text) # 숫자 아닌 값 제거; decimal point(,) 제거 + + save_results([{ + 'name': name, + 'price': first_price, + }]) + except UnexpectedAlertPresentException: + continue + +if __name__ == '__main__': + main() From 5becea81a3112b7d4a807bbd70b76d24f85aa802 Mon Sep 17 00:00:00 2001 From: twndus Date: Wed, 6 Mar 2024 01:36:58 +0900 Subject: [PATCH 016/187] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=ED=9B=84=EA=B8=B0=EC=93=B4=20=EC=9C=A0=EC=A0=80=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EC=9C=BC=EB=A1=9C=20=ED=81=AC=EB=A1=A4=EB=A7=81?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?&=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B2=84=EC=A0=80=EB=8B=9D?= =?UTF-8?q?=20=EB=B0=8F=20interaction=20=EB=B0=9C=EC=83=9D=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=EA=B8=B0=EB=A1=9D=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_review.py | 50 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/crawl_review.py b/crawl_review.py index d2f3954..ecafe3e 100644 --- a/crawl_review.py +++ b/crawl_review.py @@ -1,6 +1,7 @@ -import re +import os, re from datetime import datetime as dt +import numpy as np import pandas as pd from tqdm import tqdm @@ -8,9 +9,23 @@ from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By +from selenium.common.exceptions import UnexpectedAlertPresentException from bs4 import BeautifulSoup +def get_userid_from_recipe_reviews(): + + # filenames + crawled_files = [ + 'reviewers_240302.csv', 'reviewers_240303.csv', + 'reviewers_240304.csv', 'reviewers_240305.csv', + ] + + df = pd.concat([pd.read_csv(f) for f in crawled_files], axis=0) + unique_users = set(np.concatenate(df['reviewers'].apply(eval).values)) + + return unique_users + def get_userid_set(): # filenames RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' @@ -63,25 +78,32 @@ def save_results(data_list): date = dt.now().strftime('%y%m%d') PATH = f'reviews_{date}.csv' - - # save - df.to_csv(PATH, index=False) + if os.path.exists(PATH): + # save + df.to_csv(PATH, mode='a', index=False, header=False) + else: + df.to_csv(PATH, index=False) def main(): # get all user ids - userid_set = get_userid_set() + # userid_set = get_userid_set() + userid_set = get_userid_from_recipe_reviews() # set options for opening chrome browser in CLI env chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--headless') # headless 모드로 실행 # get automative driver - driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) + # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) + # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager(driver_version='122.0.6261.94').install())) + options = webdriver.ChromeOptions() + options.add_argument('--disable-gpu') + options.add_argument('--headless') + options.add_argument('--no-sandbox') # sandbox를 사용하지 않는다는 옵션!! 필수 + options.add_argument('--disable-blink-features=AutomationControlled') + driver = webdriver.Chrome(options=options) - # datalist - data_list = [] - # collect data by user id for i,uid in enumerate(tqdm(userid_set)): @@ -97,15 +119,17 @@ def main(): recipe_id = parse_recipe_id(review) if len(recipe_id) <= 0: continue rating = len(review.find('span', 'view2_review_star').find_all('img')) - user_history[recipe_id] = rating + datetime = review.find('span', {'style': "font-size:11px;color:#888;display: block; padding-top: 4px;"}).text + user_history[recipe_id] = {'rating': rating, 'datetime': datetime} if len(user_history) > 0: - data_list.append({ + save_results([{ 'uid': uid, 'user_name': nickname, 'history': user_history, - }) - except: + }]) + + except UnexpectedAlertPresentException: continue # save results From 4e57552b06f3b17f23c618e3d3379349034a0ce3 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 6 Mar 2024 10:15:33 +0900 Subject: [PATCH 017/187] =?UTF-8?q?fix:=20save=5Fresult=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=98=EC=98=80?= =?UTF-8?q?=EB=8A=94=EB=8D=B0=20=EB=AF=B8=EC=B2=98=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EC=9D=80=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_review.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/crawl_review.py b/crawl_review.py index ecafe3e..74693c5 100644 --- a/crawl_review.py +++ b/crawl_review.py @@ -132,8 +132,5 @@ def main(): except UnexpectedAlertPresentException: continue - # save results - save_results(data_list) - if __name__ == '__main__': main() From cd4aca574e9730604b4e857f3fd9186a18e99981 Mon Sep 17 00:00:00 2001 From: twndus Date: Wed, 6 Mar 2024 10:54:08 +0900 Subject: [PATCH 018/187] =?UTF-8?q?feat:=20=EB=B3=91=EB=A0=AC=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=9C=84=ED=95=B4=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=EC=9D=B4=EB=A6=84=EA=B3=BC=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B6=84=ED=95=A0=20?= =?UTF-8?q?#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_reviewer_split.py | 46 ++++++++++++++++++++++++++++++++++++++++ crawl_userid_split.py | 47 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 crawl_reviewer_split.py create mode 100644 crawl_userid_split.py diff --git a/crawl_reviewer_split.py b/crawl_reviewer_split.py new file mode 100644 index 0000000..af7fe01 --- /dev/null +++ b/crawl_reviewer_split.py @@ -0,0 +1,46 @@ +import numpy as np +import pandas as pd + + +def get_recipesno_set(): + # filenames + RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' + RECIPE_FILE2 = 'TB_RECIPE_SEARCH-20231130.csv' + + # read file + recipe_df_22 = pd.read_csv(RECIPE_FILE1, engine='python', encoding='cp949', encoding_errors='ignore') # EUC-KR, utf-8, cp949, ms949, iso2022_jp_2, iso2022_kr johab + recipe_df_23 = pd.read_csv(RECIPE_FILE2, engine='python', encoding='cp949', encoding_errors='ignore') + + # union recipes + recipeset_22 = set(recipe_df_22['RCP_SNO'].values) + recipeset_23 = set(recipe_df_23['RCP_SNO'].values) + recipeset_all = recipeset_22 | recipeset_23 + + print(len(recipeset_22), len(recipeset_23), len(recipeset_all)) + return recipeset_all + +def read_crawled_df(filename): + return pd.read_csv(filename) + +def main(): + # get all recipe snos + recipe_snos = get_recipesno_set() + + crawled_dfs = [] + for filename in ['reviewers_240302.csv', 'reviewers_240303.csv', 'reviewers_240304.csv']: + crawled_dfs.append(read_crawled_df(filename)) + + crawled_df = pd.concat(crawled_dfs, axis=0) + crawled_recipe_snos = set(crawled_df.recipe_sno.values) + + target_recipe_snos = recipe_snos - crawled_recipe_snos + print(len(recipe_snos)) + print(len(crawled_recipe_snos)) + print(len(target_recipe_snos)) + divided_snos = np.array_split(np.array(list(target_recipe_snos)), 4) + + for i, divided_sno in enumerate(divided_snos): + np.save(f'recipe_sno_{i}.npy', divided_sno) + +if __name__ == '__main__': + main() diff --git a/crawl_userid_split.py b/crawl_userid_split.py new file mode 100644 index 0000000..f5667a3 --- /dev/null +++ b/crawl_userid_split.py @@ -0,0 +1,47 @@ +import numpy as np +import pandas as pd + + +def get_userid_from_recipe_reviews(): + + # filenames + crawled_files = [ + 'reviewers_240302.csv', 'reviewers_240303.csv', + 'reviewers_240304.csv', 'reviewers_240305.csv', + ] + + df = pd.concat([pd.read_csv('data/'+f) for f in crawled_files], axis=0) + unique_users = set(np.concatenate(df['reviewers'].apply(eval).values)) + + return unique_users + +def get_recipesno_set(): + # filenames + RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' + RECIPE_FILE2 = 'TB_RECIPE_SEARCH-20231130.csv' + + # read file + recipe_df_22 = pd.read_csv(RECIPE_FILE1, engine='python', encoding='cp949', encoding_errors='ignore') # EUC-KR, utf-8, cp949, ms949, iso2022_jp_2, iso2022_kr johab + recipe_df_23 = pd.read_csv(RECIPE_FILE2, engine='python', encoding='cp949', encoding_errors='ignore') + + # union recipes + recipeset_22 = set(recipe_df_22['RCP_SNO'].values) + recipeset_23 = set(recipe_df_23['RCP_SNO'].values) + recipeset_all = recipeset_22 | recipeset_23 + + print(len(recipeset_22), len(recipeset_23), len(recipeset_all)) + return recipeset_all + +def read_crawled_df(filename): + return pd.read_csv(filename) + +def main(): + # get all user_ids + user_ids = get_userid_from_recipe_reviews() + divided_userids = np.array_split(np.array(list(user_ids)), 4) + + for i, divided_userid in enumerate(divided_userids): + np.save(f'userid_from_recipe_reviews_{i}.npy', divided_userid) + +if __name__ == '__main__': + main() From 6c9736f0c582f1c4c85a869435a9fc8b30b3e408 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 10:31:50 +0900 Subject: [PATCH 019/187] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #3 --- app/__init__.py | 0 app/api/__init__.py | 0 app/api/routes/__init__.py | 0 tests/__init__.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/routes/__init__.py create mode 100644 tests/__init__.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/routes/__init__.py b/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 8daca469b661cb7784efe75c1b3b6a4377434ee2 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 11:43:32 +0900 Subject: [PATCH 020/187] =?UTF-8?q?feat:=20frontend=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=9E=AC?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20#3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {app => backend}/__init__.py | 0 {app/api => backend/app}/__init__.py | 0 {app/api/routes => backend/app/api}/__init__.py | 0 backend/app/api/routes/__init__.py | 0 frontend/__init__.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename {app => backend}/__init__.py (100%) rename {app/api => backend/app}/__init__.py (100%) rename {app/api/routes => backend/app/api}/__init__.py (100%) create mode 100644 backend/app/api/routes/__init__.py create mode 100644 frontend/__init__.py diff --git a/app/__init__.py b/backend/__init__.py similarity index 100% rename from app/__init__.py rename to backend/__init__.py diff --git a/app/api/__init__.py b/backend/app/__init__.py similarity index 100% rename from app/api/__init__.py rename to backend/app/__init__.py diff --git a/app/api/routes/__init__.py b/backend/app/api/__init__.py similarity index 100% rename from app/api/routes/__init__.py rename to backend/app/api/__init__.py diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frontend/__init__.py b/frontend/__init__.py new file mode 100644 index 0000000..e69de29 From 116f931c7ffce06c1b26d3c61d9136ade400ba89 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 11:49:38 +0900 Subject: [PATCH 021/187] =?UTF-8?q?feat:=20test=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD=20#3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {tests => backend/tests}/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests => backend/tests}/__init__.py (100%) diff --git a/tests/__init__.py b/backend/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to backend/tests/__init__.py From ae3ed4f60cf444b8e7400609647638fe8f389ab2 Mon Sep 17 00:00:00 2001 From: twndus Date: Sat, 9 Mar 2024 12:09:48 +0900 Subject: [PATCH 022/187] =?UTF-8?q?feat:=20=EB=9E=AD=ED=82=B9=20=ED=83=AD?= =?UTF-8?q?=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC=ED=98=84=20=EB=B0=8F?= =?UTF-8?q?=20keyboardinterrupt=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_ranked_recipes.py | 72 +++++++++++++++++++++++++++++++++++++++++ crawl_recipe.py | 29 +++++++++++++++-- crawl_review.py | 11 ++++--- crawl_reviewer.py | 17 ++++++++-- 4 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 crawl_ranked_recipes.py diff --git a/crawl_ranked_recipes.py b/crawl_ranked_recipes.py new file mode 100644 index 0000000..f28f166 --- /dev/null +++ b/crawl_ranked_recipes.py @@ -0,0 +1,72 @@ +from datetime import datetime as dt + +from tqdm import tqdm +import numpy as np + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By +from selenium.common.exceptions import UnexpectedAlertPresentException + +from bs4 import BeautifulSoup + +def get_html_source(driver, rank_standard:str='r', period:str='d'): + + url = f"https://www.10000recipe.com/ranking/home_new.html?dtype={period}&rtype={rank_standard}" + driver.get(url) # url 접속 + driver.implicitly_wait(2) + + return driver.page_source + +def main(): + # chrome driver + options = webdriver.ChromeOptions() + options.add_argument('--disable-gpu') + options.add_argument('--headless') + options.add_argument('--no-sandbox') # sandbox를 사용하지 않는다는 옵션!! 필수 + options.add_argument('--disable-blink-features=AutomationControlled') + + driver = webdriver.Chrome(options=options) + + # only ranked recipes + # for 주간 월간 일간 + crawled_ids = [] + + rank_standard = 'c' + for period in ['d', 'w', 'm']: + + # 레시피 랭킹 페이지 읽어 + html_source = get_html_source(driver, rank_standard=rank_standard, period=period) + + # make soup obj + soup = BeautifulSoup(html_source, 'html.parser') + + # parse recipe snos + if rank_standard == 'r': + ##
+ ## + for items in tqdm(soup.find_all('li', 'common_sp_list_li')): + crawled_id = items.find('div', 'common_sp_thumb').find('a', 'common_sp_link').get('href').split('/')[-1] + crawled_ids.append(crawled_id) + elif rank_standard == 'c': + ## ul class="goods_best4_1" + ## div class="best_pic" + ## a href="/profile/index.html?uid=minimini0107" + for items in tqdm(soup.find('ul', 'goods_best4_1').find_all('li')): + crawled_id = items.find('div', 'best_pic').find('a').get('href').split('uid=')[-1] + crawled_ids.append(crawled_id) + + # numpy 로 저장 + if rank_standard == 'r': + crawled_ids = np.unique(np.array(crawled_ids)) + filename = f'ranked_recipe_snos-{dt.now().strftime("%y%m%d")}.npy' + elif rank_standard == 'c': + crawled_ids = np.unique(np.array(crawled_ids)) + filename = f'ranked_chef_uids-{dt.now().strftime("%y%m%d")}.npy' + + np.save(filename, crawled_ids) + + +if __name__ == '__main__': + main() diff --git a/crawl_recipe.py b/crawl_recipe.py index 710dc7c..be77cf5 100644 --- a/crawl_recipe.py +++ b/crawl_recipe.py @@ -1,6 +1,7 @@ import re, os from datetime import datetime as dt +import numpy as np import pandas as pd from tqdm import tqdm @@ -12,6 +13,18 @@ from bs4 import BeautifulSoup +def get_userid_from_recipe_reviews(): + + # filenames + crawled_files = [ + 'reviewers_240309.csv' + ] + + df = pd.concat([pd.read_csv(f) for f in crawled_files], axis=0) + unique_users = set(np.concatenate(df['reviewers'].apply(eval).values)) + + return unique_users + def get_userid_set(): # filenames RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' @@ -72,14 +85,21 @@ def save_results(data_list): def main(): # get all user ids - recipeid_set = get_userid_set() + # recipeid_set = get_userid_set() + recipeid_set = get_userid_from_recipe_reviews() # set options for opening chrome browser in CLI env chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--headless') # headless 모드로 실행 # get automative driver - driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) + options = webdriver.ChromeOptions() + options.add_argument('--disable-gpu') + options.add_argument('--headless') + options.add_argument('--no-sandbox') # sandbox를 사용하지 않는다는 옵션!! 필수 + options.add_argument('--disable-blink-features=AutomationControlled') + + driver = webdriver.Chrome(options=options) # collect data by user id for i,uid in enumerate(tqdm(recipeid_set)): @@ -109,8 +129,11 @@ def main(): 'uid': uid, 'recipes': user_recipes, }]) - except UnexpectedAlertPresentException: + except KeyboardInterrupt: + break + except: continue + if __name__ == '__main__': main() diff --git a/crawl_review.py b/crawl_review.py index 74693c5..e62bd4a 100644 --- a/crawl_review.py +++ b/crawl_review.py @@ -17,8 +17,9 @@ def get_userid_from_recipe_reviews(): # filenames crawled_files = [ - 'reviewers_240302.csv', 'reviewers_240303.csv', - 'reviewers_240304.csv', 'reviewers_240305.csv', +# 'reviewers_240302.csv', 'reviewers_240303.csv', +# 'reviewers_240304.csv', 'reviewers_240305.csv', + 'reviewers_240309.csv' ] df = pd.concat([pd.read_csv(f) for f in crawled_files], axis=0) @@ -95,8 +96,6 @@ def main(): chrome_options.add_argument('--headless') # headless 모드로 실행 # get automative driver - # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) - # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager(driver_version='122.0.6261.94').install())) options = webdriver.ChromeOptions() options.add_argument('--disable-gpu') options.add_argument('--headless') @@ -129,7 +128,9 @@ def main(): 'history': user_history, }]) - except UnexpectedAlertPresentException: + except KeyboardInterrupt: + break + except: continue if __name__ == '__main__': diff --git a/crawl_reviewer.py b/crawl_reviewer.py index f3c799d..55a4972 100644 --- a/crawl_reviewer.py +++ b/crawl_reviewer.py @@ -1,6 +1,8 @@ import os from datetime import datetime as dt + from tqdm import tqdm +import numpy as np import pandas as pd from selenium import webdriver @@ -10,6 +12,10 @@ from bs4 import BeautifulSoup +def get_ranked_recipesno_set(): + filename = 'ranked_recipe_snos-240309.npy' + return np.load(filename) + def get_recipesno_set(): # filenames RECIPE_FILE1 = 'TB_RECIPE_SEARCH-220701.csv' @@ -51,15 +57,20 @@ def save_results(data_list): def main(): # get all recipe snos - recipe_snos = get_recipesno_set() + #recipe_snos = get_recipesno_set() + recipe_snos = get_ranked_recipesno_set() # set options for opening chrome browser in CLI env chrome_options = webdriver.ChromeOptions() chrome_options.add_argument('--headless') # headless 모드로 실행 # get automative driver - # driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install())) - driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=chrome_options) + options = webdriver.ChromeOptions() + options.add_argument('--disable-gpu') + options.add_argument('--headless') + options.add_argument('--no-sandbox') # sandbox를 사용하지 않는다는 옵션!! 필수 + options.add_argument('--disable-blink-features=AutomationControlled') + driver = webdriver.Chrome(options=options) # collect data by recipe snos for i, rsno in enumerate(tqdm(recipe_snos)): From c99496410cf498155b922eb82f43601f33b1ccb4 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 14:43:55 +0900 Subject: [PATCH 023/187] =?UTF-8?q?feat:=20.env=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=B0=BE=EA=B8=B0=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/database/__init__.py | 0 backend/app/database/data_source.py | 34 +++++++++++++++++++++++++ backend/app/database/dev.env | 4 +++ backend/app/database/prod.env | 4 +++ backend/tests/database/__init__.py | 0 backend/tests/database/test_database.py | 11 ++++++++ 6 files changed, 53 insertions(+) create mode 100644 backend/app/database/__init__.py create mode 100644 backend/app/database/data_source.py create mode 100644 backend/app/database/dev.env create mode 100644 backend/app/database/prod.env create mode 100644 backend/tests/database/__init__.py create mode 100644 backend/tests/database/test_database.py diff --git a/backend/app/database/__init__.py b/backend/app/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/database/data_source.py b/backend/app/database/data_source.py new file mode 100644 index 0000000..3eb477c --- /dev/null +++ b/backend/app/database/data_source.py @@ -0,0 +1,34 @@ +import os + +from pymongo import MongoClient +from pymongo.collection import Collection +from dotenv import load_dotenv +from pydantic import BaseModel + +def env_file_of(env: str) -> str: + return os.path.join(os.path.dirname(os.path.abspath(__file__)), f"{env}.env") + +class DataSource(BaseModel): + host: str + port: str + database: str + + def make_connection(self) -> None: + self.client = MongoClient(f"mongodb://{self.host}:{self.port}/") + + def collection_with_name_as(self, name: str) -> Collection: + return self.client[self.database][name] + +env_name = os.getenv('ENV', 'dev') +file_path = env_file_of(env_name) +assert os.path.exists(file_path), f"파일이 존재하지 않습니다: {file_path}" + +load_dotenv(file_path) + +data_env = { + 'host': os.getenv('HOST'), + 'port': os.getenv('PORT'), + 'database': os.getenv('DATABASE'), +} + +data_source = DataSource(**data_env) diff --git a/backend/app/database/dev.env b/backend/app/database/dev.env new file mode 100644 index 0000000..21fabab --- /dev/null +++ b/backend/app/database/dev.env @@ -0,0 +1,4 @@ +ENV=dev +HOST=localhost +PORT=27017 +DATABASE=dev diff --git a/backend/app/database/prod.env b/backend/app/database/prod.env new file mode 100644 index 0000000..7f55659 --- /dev/null +++ b/backend/app/database/prod.env @@ -0,0 +1,4 @@ +ENV=prod +HOST=10.0.6.6 +PORT=27017 +DATABASE=prod diff --git a/backend/tests/database/__init__.py b/backend/tests/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/database/test_database.py b/backend/tests/database/test_database.py new file mode 100644 index 0000000..ac71dd6 --- /dev/null +++ b/backend/tests/database/test_database.py @@ -0,0 +1,11 @@ +from ...app.database.data_source import DataSource, env_file_of + +def test_환경변수파일_경로찾기_정상(): + # given + env = 'dev' + + # when + filename = env_file_of(env).split('/')[-1] + + # then + assert filename == 'dev.env' From 14e4eb0a30aaaa4005117062d0d05c9e95defb7c Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 14:44:26 +0900 Subject: [PATCH 024/187] =?UTF-8?q?feat:=20.gitignore=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..330f0e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,210 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,linux \ No newline at end of file From cf83f8e5115703e97b8a4db945799866d5e02f27 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 14:49:23 +0900 Subject: [PATCH 025/187] =?UTF-8?q?refactor:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=ED=8C=8C=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20parametrize=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5 --- backend/tests/database/test_database.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/tests/database/test_database.py b/backend/tests/database/test_database.py index ac71dd6..66cb136 100644 --- a/backend/tests/database/test_database.py +++ b/backend/tests/database/test_database.py @@ -1,11 +1,17 @@ +import pytest + from ...app.database.data_source import DataSource, env_file_of -def test_환경변수파일_경로찾기_정상(): +@pytest.mark.parametrize("input, output", [ + ('dev', 'dev.env'), + ('prod', 'prod.env'), +]) +def test_환경변수파일_경로찾기_정상(input, output): # given - env = 'dev' + env = input # when filename = env_file_of(env).split('/')[-1] # then - assert filename == 'dev.env' + assert filename == output From 4404e144547b6b70015265b533ab4f609ca55923 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 15:18:12 +0900 Subject: [PATCH 026/187] =?UTF-8?q?test:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EC=86=8C=EC=8A=A4=20=EC=83=9D=EC=84=B1=20=EC=A0=95=EC=83=81=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5 --- backend/tests/database/test_database.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend/tests/database/test_database.py b/backend/tests/database/test_database.py index 66cb136..c464ddf 100644 --- a/backend/tests/database/test_database.py +++ b/backend/tests/database/test_database.py @@ -15,3 +15,19 @@ def test_환경변수파일_경로찾기_정상(input, output): # then assert filename == output + +@pytest.mark.parametrize("host, port, database", [ + ('localhost', '27017', 'dev'), + ('10.0.6.6', '27017', 'prod'), +]) +def test_데이터소스_생성_정상(host, port, database): + # given + config = { + 'host': host, + 'port': port, + 'database': database + } + + # when, then + with pytest.raises(Exception): + DataSource(**config) \ No newline at end of file From 97f21800ec70c352e281613533e7e1eda8c85a0f Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 15:47:43 +0900 Subject: [PATCH 027/187] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EB=B0=98=ED=99=98=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5 --- backend/app/database/data_source.py | 22 +++++++++++++------ backend/tests/assert_not_raises.py | 13 ++++++++++++ backend/tests/database/test_database.py | 28 ++++++++++++++++++++++--- 3 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 backend/tests/assert_not_raises.py diff --git a/backend/app/database/data_source.py b/backend/app/database/data_source.py index 3eb477c..d907b4d 100644 --- a/backend/app/database/data_source.py +++ b/backend/app/database/data_source.py @@ -2,8 +2,10 @@ from pymongo import MongoClient from pymongo.collection import Collection +from pymongo.database import Database from dotenv import load_dotenv from pydantic import BaseModel +from typing import Optional def env_file_of(env: str) -> str: return os.path.join(os.path.dirname(os.path.abspath(__file__)), f"{env}.env") @@ -11,13 +13,21 @@ def env_file_of(env: str) -> str: class DataSource(BaseModel): host: str port: str - database: str + database_name: str + client: Optional[object] = None - def make_connection(self) -> None: - self.client = MongoClient(f"mongodb://{self.host}:{self.port}/") - def collection_with_name_as(self, name: str) -> Collection: - return self.client[self.database][name] + if self.client is None: + self._make_connection() + return self.client[self.database_name][name] + + def database(self) -> Database: + if self.client is None: + self._make_connection() + return self.client[self.database_name] + + def _make_connection(self) -> None: + self.client = MongoClient(f"mongodb://{self.host}:{self.port}/") env_name = os.getenv('ENV', 'dev') file_path = env_file_of(env_name) @@ -28,7 +38,7 @@ def collection_with_name_as(self, name: str) -> Collection: data_env = { 'host': os.getenv('HOST'), 'port': os.getenv('PORT'), - 'database': os.getenv('DATABASE'), + 'database_name': os.getenv('DATABASE'), } data_source = DataSource(**data_env) diff --git a/backend/tests/assert_not_raises.py b/backend/tests/assert_not_raises.py new file mode 100644 index 0000000..962e81d --- /dev/null +++ b/backend/tests/assert_not_raises.py @@ -0,0 +1,13 @@ +from contextlib import contextmanager + + +@contextmanager +def not_raises(ExpectedException): + try: + yield + + except ExpectedException as error: + raise AssertionError(f"Raised exception {error} when it should not!") + + except Exception as error: + raise AssertionError(f"An unexpected exception {error} raised.") \ No newline at end of file diff --git a/backend/tests/database/test_database.py b/backend/tests/database/test_database.py index c464ddf..60ebf65 100644 --- a/backend/tests/database/test_database.py +++ b/backend/tests/database/test_database.py @@ -1,6 +1,7 @@ import pytest from ...app.database.data_source import DataSource, env_file_of +from ..assert_not_raises import not_raises @pytest.mark.parametrize("input, output", [ ('dev', 'dev.env'), @@ -16,6 +17,7 @@ def test_환경변수파일_경로찾기_정상(input, output): # then assert filename == output + @pytest.mark.parametrize("host, port, database", [ ('localhost', '27017', 'dev'), ('10.0.6.6', '27017', 'prod'), @@ -25,9 +27,29 @@ def test_데이터소스_생성_정상(host, port, database): config = { 'host': host, 'port': port, - 'database': database + 'database_name': database } # when, then - with pytest.raises(Exception): - DataSource(**config) \ No newline at end of file + with not_raises(Exception): + DataSource(**config) + + +@pytest.mark.parametrize("host, port, database", [ + ('localhost', '27017', 'dev'), + ('10.0.6.6', '27017', 'prod'), +]) +def test_데이터베이스_반환_정상(host, port, database): + # given + config = { + 'host': host, + 'port': port, + 'database_name': database + } + datasource = DataSource(**config) + + # when + result = datasource.database() + + # then + assert result is not None From b95ba461094c57a50e390e77bdd71f0b649623c6 Mon Sep 17 00:00:00 2001 From: twndus Date: Sat, 9 Mar 2024 19:03:34 +0900 Subject: [PATCH 028/187] =?UTF-8?q?feat(RecommendationPage,-utils):=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20=EB=A9=94=EB=89=B4=EB=B0=94=20=EB=B0=8F=20?= =?UTF-8?q?RecommendationPage=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 버튼에 링크 연결은 되어 있지 않음 #6 --- frontend/Home.py | 14 ++++++ frontend/pages/RecommendationPage.py | 58 +++++++++++++++++++++++++ frontend/utils.py | 64 ++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 frontend/Home.py create mode 100644 frontend/pages/RecommendationPage.py create mode 100644 frontend/utils.py diff --git a/frontend/Home.py b/frontend/Home.py new file mode 100644 index 0000000..9e10901 --- /dev/null +++ b/frontend/Home.py @@ -0,0 +1,14 @@ +# Temporal Entrypoint +import streamlit as st + +from utils import menu_tab + +# page labeling +st.set_page_config( + page_title="Entrypoint", + page_icon="🛒", +) + + +# 상단 메뉴 +menu_tab(login=True, user='Judy') diff --git a/frontend/pages/RecommendationPage.py b/frontend/pages/RecommendationPage.py new file mode 100644 index 0000000..1f393ee --- /dev/null +++ b/frontend/pages/RecommendationPage.py @@ -0,0 +1,58 @@ +import streamlit as st +import pandas as pd +import numpy as np +import time + +from utils import menu_tab + +user = '주디' + +# page labeling +st.set_page_config( + page_title="Hello", + page_icon="👋", +) + +# 상단바 +menu_tab(login=True, user='Judy') + +# 페이지 구성 +container = st.container(border=True) + +with container: + st.markdown("

이번 주 장바구니 만들기

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + st.markdown("
예산을 정해주세요.
", unsafe_allow_html=True) + + cols = st.columns([1,5,1]) + with cols[1]: + price = st.slider( + label='', + min_value=10000, + max_value=1000000, + value=50000, + step=5000 + ) + + cols = st.columns(5) + with cols[2]: + st.write("예산: ", price, '원') + + + with cols[1]: + button1 = st.button("이전 장바구니 보기") + + with cols[3]: + button2 = st.button("다음 장바구니 보기", type="primary") + + st.markdown( + """""", + unsafe_allow_html=True, + ) diff --git a/frontend/utils.py b/frontend/utils.py new file mode 100644 index 0000000..29bf3df --- /dev/null +++ b/frontend/utils.py @@ -0,0 +1,64 @@ +import streamlit as st + +def menu_tab(login=False, user=None): + # layout + cols = st.columns([1,2]) + + with cols[0]: + st.markdown('', unsafe_allow_html=True) + + with cols[1]: + cols2 = st.columns([5,5,4,4]) + with cols2[0]: + st.markdown('', unsafe_allow_html=True) + with cols2[1]: + st.markdown('', unsafe_allow_html=True) + with cols2[2]: + st.markdown('', unsafe_allow_html=True) + with cols2[3]: + if login: + st.markdown(f'', unsafe_allow_html=True) + else: + st.markdown(f'', unsafe_allow_html=True) + + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) From 03bbcf9d04332d0412fcf32616bae32b877b3720 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sat, 9 Mar 2024 19:42:14 +0900 Subject: [PATCH 029/187] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EB=B0=8F=20=EC=BB=AC=EB=A0=89?= =?UTF-8?q?=EC=85=98=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5 --- backend/app/database/data_source.py | 30 ++++++++++++++--- backend/app/exception/database/__init__.py | 0 .../exception/database/database_exception.py | 11 +++++++ backend/tests/database/test_database.py | 32 ++++++++++++++++--- 4 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 backend/app/exception/database/__init__.py create mode 100644 backend/app/exception/database/database_exception.py diff --git a/backend/app/database/data_source.py b/backend/app/database/data_source.py index d907b4d..43163fa 100644 --- a/backend/app/database/data_source.py +++ b/backend/app/database/data_source.py @@ -7,6 +7,10 @@ from pydantic import BaseModel from typing import Optional +from ..exception.database.database_exception import ( + DatabaseNotFoundException, CollectionNotFoundException +) + def env_file_of(env: str) -> str: return os.path.join(os.path.dirname(os.path.abspath(__file__)), f"{env}.env") @@ -16,18 +20,34 @@ class DataSource(BaseModel): database_name: str client: Optional[object] = None - def collection_with_name_as(self, name: str) -> Collection: + def database(self) -> Database: if self.client is None: self._make_connection() - return self.client[self.database_name][name] + + self._validate_database() + return self.client[self.database_name] - def database(self) -> Database: + def collection_with_name_as(self, collection_name: str) -> Collection: if self.client is None: self._make_connection() - return self.client[self.database_name] + + self._validate_collection(collection_name) + return self.client[self.database_name][collection_name] def _make_connection(self) -> None: - self.client = MongoClient(f"mongodb://{self.host}:{self.port}/") + url = f"mongodb://{self.host}:{self.port}/" + self.client = MongoClient(url) + + def _validate_database(self) -> None: + database_names = self.client.list_database_names() + if self.database_name not in database_names: + raise DatabaseNotFoundException(f"해당하는 데이터베이스가 존재하지 않습니다: {self.database_name}") + + def _validate_collection(self, collection_name) -> None: + collection_names = self.client[self.database_name].list_collection_names() + if collection_name not in collection_names: + raise CollectionNotFoundException(f"해당하는 컬렉션이 존재하지 않습니다: {collection_name}") + env_name = os.getenv('ENV', 'dev') file_path = env_file_of(env_name) diff --git a/backend/app/exception/database/__init__.py b/backend/app/exception/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/exception/database/database_exception.py b/backend/app/exception/database/database_exception.py new file mode 100644 index 0000000..367724c --- /dev/null +++ b/backend/app/exception/database/database_exception.py @@ -0,0 +1,11 @@ +class DatabaseException(Exception): + def __init__(self, message): + super().__init__(message) + +class DatabaseNotFoundException(DatabaseException): + def __init__(self, message): + super().__init__(message) + +class CollectionNotFoundException(DatabaseException): + def __init__(self, message): + super().__init__(message) diff --git a/backend/tests/database/test_database.py b/backend/tests/database/test_database.py index 60ebf65..b7e17a6 100644 --- a/backend/tests/database/test_database.py +++ b/backend/tests/database/test_database.py @@ -35,11 +35,35 @@ def test_데이터소스_생성_정상(host, port, database): DataSource(**config) -@pytest.mark.parametrize("host, port, database", [ +@pytest.mark.parametrize("host, port, database_name", [ ('localhost', '27017', 'dev'), ('10.0.6.6', '27017', 'prod'), ]) -def test_데이터베이스_반환_정상(host, port, database): +def test_데이터베이스_반환_정상(host, port, database_name): + # given + config = { + 'host': host, + 'port': port, + 'database_name': database_name + } + datasource = DataSource(**config) + + # when + database = datasource.database() + + # then + assert database is not None + + +@pytest.mark.parametrize("host, port, database, collection_name" , [ + ('localhost', '27017', 'dev', 'users'), + ('localhost', '27017', 'dev', 'recipes'), + ('localhost', '27017', 'dev', 'ingredients'), + ('10.0.6.6', '27017', 'prod', 'users'), + ('10.0.6.6', '27017', 'prod', 'recipes'), + ('10.0.6.6', '27017', 'prod', 'ingredients'), +]) +def test_컬렉션_반환_정상(host, port, database, collection_name): # given config = { 'host': host, @@ -49,7 +73,7 @@ def test_데이터베이스_반환_정상(host, port, database): datasource = DataSource(**config) # when - result = datasource.database() + collection = datasource.collection_with_name_as(collection_name) # then - assert result is not None + assert collection is not None From aab5c7e13f51a649f3eff937ebeeb55f05d48b17 Mon Sep 17 00:00:00 2001 From: twndus Date: Sun, 10 Mar 2024 18:02:40 +0900 Subject: [PATCH 030/187] =?UTF-8?q?feat(RecommendationHistoryPage):=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #7 --- frontend/pages/RecommendationHistoryPage.py | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 frontend/pages/RecommendationHistoryPage.py diff --git a/frontend/pages/RecommendationHistoryPage.py b/frontend/pages/RecommendationHistoryPage.py new file mode 100644 index 0000000..583465d --- /dev/null +++ b/frontend/pages/RecommendationHistoryPage.py @@ -0,0 +1,92 @@ +import streamlit as st +import pandas as pd +import numpy as np +import time + +from utils import menu_tab + +user = '주디' + +# page labeling +st.set_page_config( + page_title="RecommendationHistoryPage", +) + +# 상단바 +menu_tab(login=True, user='Judy') + +# 페이지 구성 +container = st.container(border=True) + +with container: + + st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) + + sub_container = st.container(border=False) + with sub_container: + st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) + st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) + + + foods = [ + '어묵김말이', + '두부새우전', + '알밥', + '현미호두죽', + ] + + img_urls = [ + 'https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg', + 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg', + 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg', + 'https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg', + ] + + recipe_urls = [ + 'https://www.10000recipe.com/recipe/128671', + 'https://www.10000recipe.com/recipe/128892', + 'https://www.10000recipe.com/recipe/128932', + 'https://www.10000recipe.com/recipe/131871', + ] + + feedback = [ True, False, False, True ] + + for url in range(int(len(img_urls)/4)): + cols = st.columns(4) + + for i in range(4): + with cols[i]: + st.markdown(f'
Your Image', unsafe_allow_html=True) + + sub_cols = st.columns([3,1]) + with sub_cols[0]: + st.markdown(f'

{foods[i]}

', unsafe_allow_html=True) + + if feedback[i]: + btn_label = '❤️' + else: + btn_label = '🩶' + with sub_cols[-1]: + st.markdown(f'', unsafe_allow_html=True) + # POST /api/users/{user_id}/foods + # inputs: user_id, List[food_id] + + +st.markdown(""" + + """, unsafe_allow_html=True) From 56f8f915c3a238b500f9f5ccad51de55d56ce2e4 Mon Sep 17 00:00:00 2001 From: twndus Date: Sun, 10 Mar 2024 18:04:05 +0900 Subject: [PATCH 031/187] =?UTF-8?q?feat(ResultPage1):=20ResultPage1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20&=20=EC=9E=A5=EB=B0=94=EA=B5=AC=EB=8B=88?= =?UTF-8?q?=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EA=B8=B0=EB=8A=A5=EC=9D=80=20?= =?UTF-8?q?utils=EC=97=90=20=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #9 --- frontend/pages/ResultPage1.py | 132 ++++++++++++++++++++++++++++++++++ frontend/utils.py | 9 +++ 2 files changed, 141 insertions(+) create mode 100644 frontend/pages/ResultPage1.py diff --git a/frontend/pages/ResultPage1.py b/frontend/pages/ResultPage1.py new file mode 100644 index 0000000..2802c82 --- /dev/null +++ b/frontend/pages/ResultPage1.py @@ -0,0 +1,132 @@ +import streamlit as st +import pandas as pd +import numpy as np +import time + +from utils import menu_tab, basket_feedback + +user = '주디' +ingredients = [ + {'ingredient_name': '브로콜리', + 'amount': 1, 'unit': 'kg', + 'price': 4680, + 'img_url': 'https://health.chosun.com/site/data/img_dir/2024/01/19/2024011902009_0.jpg', + 'market_url': 'https://www.coupang.com/vp/products/4874444452?itemId=6339533080&vendorItemId=73634892616&pickType=COU_PICK&q=%EB%B8%8C%EB%A1%9C%EC%BD%9C%EB%A6%AC&itemsCount=36&searchId=891d0b69dc8f452daf392e3db2482732&rank=1&isAddedCart='}, + {'ingredient_name': '초고추장', + 'amount': 500, 'unit': 'g', + 'price': 5000, + 'img_url': 'https://image7.coupangcdn.com/image/retail/images/4810991441045098-31358d86-eff6-45f4-8ed6-f36b642e8944.jpg', + 'market_url': 'https://www.coupang.com/vp/products/6974484284?itemId=17019959259&vendorItemId=3000138402&q=%EC%B4%88%EA%B3%A0%EC%B6%94%EC%9E%A5&itemsCount=36&searchId=d5538b6e86d04be3938c98ef1655df85&rank=1&isAddedCart='}, + ] + +# page labeling +st.set_page_config( + page_title="ResultPage-1", +) + +# 상단바 +menu_tab(login=True, user='Judy') + +# 페이지 구성 +container = st.container(border=True) + +with container: + + st.markdown("

새로운 장바구니를 추천받았어요!

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + + st.divider() + + st.markdown("

추천 장바구니

", unsafe_allow_html=True) + + total_price = 0 + + for ingredient in ingredients: + sub_container = st.container(border=True) + + with sub_container: + + cols = st.columns(5) + with cols[0]: + st.image(ingredient['img_url']) + with cols[1]: + st.write(ingredient['ingredient_name']) + st.write(ingredient['amount'], ingredient['unit']) + + with cols[-1]: + st.link_button('구매', ingredient['market_url'], type='primary') + + total_price += ingredient['price'] + + st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) + + st.divider() + + st.markdown("

이 장바구니로 만들 수 있는 음식 레시피

", unsafe_allow_html=True) + + foods = [ + '어묵김말이', + '두부새우전', + '알밥', + '현미호두죽', + ] + + img_urls = [ + 'https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg', + 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg', + 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg', + 'https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg', + ] + + recipe_urls = [ + 'https://www.10000recipe.com/recipe/128671', + 'https://www.10000recipe.com/recipe/128892', + 'https://www.10000recipe.com/recipe/128932', + 'https://www.10000recipe.com/recipe/131871', + ] + + feedback = [ True, False, False, True ] + + for url in range(int(len(img_urls)/4)): + cols = st.columns(4) + + for i in range(4): + with cols[i]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + sub_cols = st.columns([3,1]) + with sub_cols[0]: + st.markdown(f'

{foods[i]}

', unsafe_allow_html=True) + + if feedback[i]: + btn_label = '❤️' + else: + btn_label = '🩶' + with sub_cols[-1]: + st.markdown(f'', unsafe_allow_html=True) + # POST /api/users/{user_id}/foods + # inputs: user_id, List[food_id] + + st.text("\n\n") + + basket_feedback() + + +st.markdown(""" + + """, unsafe_allow_html=True) diff --git a/frontend/utils.py b/frontend/utils.py index 29bf3df..c31ca61 100644 --- a/frontend/utils.py +++ b/frontend/utils.py @@ -62,3 +62,12 @@ def menu_tab(login=False, user=None): """, unsafe_allow_html=True, ) + +def basket_feedback(): + st.markdown("
방금 추천받은 장바구니 어땠나요?
", unsafe_allow_html=True) + st.text("") + cols = st.columns([3,1,1,3]) + with cols[1]: + st.button('좋아요') + with cols[2]: + st.button('싫어요') From ac38b2f6579e24624eefc58f1f457e340754a011 Mon Sep 17 00:00:00 2001 From: twndus Date: Sun, 10 Mar 2024 21:59:56 +0900 Subject: [PATCH 032/187] =?UTF-8?q?feat(pages/RecommendationHistoryPage):?= =?UTF-8?q?=20mock=20API=EB=A1=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=84=EC=86=A1=ED=95=98=EA=B3=A0=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=9D=84=20=EB=B0=9B=EC=95=84=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6 --- frontend/pages/RecommendationHistoryPage.py | 60 +++++++++++++-------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/frontend/pages/RecommendationHistoryPage.py b/frontend/pages/RecommendationHistoryPage.py index 583465d..86e8877 100644 --- a/frontend/pages/RecommendationHistoryPage.py +++ b/frontend/pages/RecommendationHistoryPage.py @@ -4,8 +4,11 @@ import time from utils import menu_tab +import requests user = '주디' +user_id = '1' +page_num = '1' # page labeling st.set_page_config( @@ -15,6 +18,14 @@ # 상단바 menu_tab(login=True, user='Judy') +# RecommendationHistoryByPage request +response = requests.get(f"https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes?page={page_num}") +if response.status_code == 200: + data = response.json()['food_list'] +else: + print(f'status code: {response.status_code}') + data = None + # 페이지 구성 container = st.container(border=True) @@ -23,44 +34,47 @@ st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) sub_container = st.container(border=False) + with sub_container: st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) - foods = [ - '어묵김말이', - '두부새우전', - '알밥', - '현미호두죽', - ] - - img_urls = [ - 'https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg', - 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg', - 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg', - 'https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg', - ] - - recipe_urls = [ - 'https://www.10000recipe.com/recipe/128671', - 'https://www.10000recipe.com/recipe/128892', - 'https://www.10000recipe.com/recipe/128932', - 'https://www.10000recipe.com/recipe/131871', - ] +# foods = [ +# '어묵김말이', +# '두부새우전', +# '알밥', +# '현미호두죽', +# ] +# +# img_urls = [ +# 'https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg', +# 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg', +# 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg', +# 'https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg', +# ] +# +# recipe_urls = [ +# 'https://www.10000recipe.com/recipe/128671', +# 'https://www.10000recipe.com/recipe/128892', +# 'https://www.10000recipe.com/recipe/128932', +# 'https://www.10000recipe.com/recipe/131871', +# ] feedback = [ True, False, False, True ] - for url in range(int(len(img_urls)/4)): + #for url in range(int(len(img_urls)/4)): + for row in range(int(len(data)/4)): cols = st.columns(4) for i in range(4): + if row == len(data)//4: i = len(data)%4 with cols[i]: - st.markdown(f'Your Image', unsafe_allow_html=True) + st.markdown(f'Your Image', unsafe_allow_html=True) sub_cols = st.columns([3,1]) with sub_cols[0]: - st.markdown(f'

{foods[i]}

', unsafe_allow_html=True) + st.markdown(f'

{data[i]["food_name"]}

', unsafe_allow_html=True) if feedback[i]: btn_label = '❤️' From 46313afcce9499fad022083cf0da73dc5cea9669 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 11 Mar 2024 12:04:06 +0900 Subject: [PATCH 033/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=95=84?= =?UTF-8?q?=EC=9A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11 --- backend/tests/api/routes/users/test_users.py | 111 +++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 backend/tests/api/routes/users/test_users.py diff --git a/backend/tests/api/routes/users/test_users.py b/backend/tests/api/routes/users/test_users.py new file mode 100644 index 0000000..f120c46 --- /dev/null +++ b/backend/tests/api/routes/users/test_users.py @@ -0,0 +1,111 @@ +import pytest + +def test_회원가입_필수입력누락_오류(): + # given + # POST /api/users + # 입력: login_id, login_password, nickname, email + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + pass + +def test_회원가입_필수입력검증_오류(): + # given + # POST /api/users + # 입력: login_id, login_password, nickname, email + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + pass + +def test_회원가입_아이디중복_오류(): + # GET /api/users?login_id={login_id} + # - 400 Bad Request: + # 409 Conflict: 이미 존재하는 아이디인 경우 + pass + +def test_회원가입_닉네임중복_오류(): + # GET /api/users?nickname={nickname} + # 409 Conflict: 이미 존재하는 닉네임인 경우 + pass + +def test_회원가입_정상(): + # POST /api/users + # 입력: login_id, login_password, nickname, email + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + + pass + +def test_로그인_필수입력누락_오류(): + # POST /api/users/auths + # - 입력: login_id, login_password + # 출력: session id, 201 Created + pass + +def test_로그인_필수입력검증_오류(): + # POST /api/users/auths + # - 입력: login_id, login_password + # 출력: session id, 201 Created + pass + +def test_로그인_로그인아이디미존재_오류(): + # POST /api/users/auths + # - 입력: login_id, login_password + # 출력: session id, 201 Created + pass + +def test_로그인_비밀번호불일치_오류(): + # POST /api/users/auths + # - 입력: login_id, login_password + # 출력: session id, 201 Created + pass + +def test_로그인_정상(): + # POST /api/users/auths + # - 입력: login_id, login_password + # 출력: session id, 201 Created + pass + +def test_선호음식_리스트조회_미로그인_오류(): + # GET /api/foods?page={page_num} + # 출력: List[음식], next_page_url 200 OK(없을 경우 비운채로) + # 401 Unauthorized: 로그인 안 한 상태 + pass + +def test_선호음식_리스트조회_정상(): + # GET /api/foods?page={page_num} + # 출력: List[음식], next_page_url 200 OK(없을 경우 비운채로) + # 401 Unauthorized: 로그인 안 한 상태 + pass + +def test_선호음식_리스트조회_페이지네이션(): + # GET /api/foods?page={page_num} + # 출력: List[음식], next_page_url 200 OK(없을 경우 비운채로) + # 401 Unauthorized: 로그인 안 한 상태 + pass + +def test_선호음식저장_개수부족_오류(): + # POST /api/users/{user_id}/foods + # 입력: user_id, List[food_id] + # 출력: 201 Created + # 에러 + # 400 Bad Request: 입력값 누락(필요 최소 재료개수 등) + # 401 Unauthorized: 로그인 안 한 상태 + # 500 Internal Server Error: 관리자에게 문의하세요. + pass + +def test_선호음식저장_미로그인_오류(): + # POST /api/users/{user_id}/foods + # 입력: user_id, List[food_id] + # 출력: 201 Created + # 에러 + # 400 Bad Request: 입력값 누락(필요 최소 재료개수 등) + # 401 Unauthorized: 로그인 안 한 상태 + # 500 Internal Server Error: 관리자에게 문의하세요. + pass + +def test_선호음식저장_정상(): + # POST /api/users/{user_id}/foods + # 입력: user_id, List[food_id] + # 출력: 201 Created + # 에러 + # 400 Bad Request: 입력값 누락(필요 최소 재료개수 등) + # 401 Unauthorized: 로그인 안 한 상태 + # 500 Internal Server Error: 관리자에게 문의하세요. + pass From 29a16a694f45fb205c4f8b96bbb55f50900b384a Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 11 Mar 2024 13:37:40 +0900 Subject: [PATCH 034/187] =?UTF-8?q?test:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11 --- backend/app/exception/users/__init__.py | 0 .../app/exception/users/signup_exeption.py | 49 ++++++ backend/app/exception/users/user_exception.py | 6 + backend/tests/api/__init__.py | 0 backend/tests/api/routes/__init__.py | 0 backend/tests/api/routes/users/__init__.py | 0 backend/tests/api/routes/users/test_users.py | 151 +++++++++++++++++- 7 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 backend/app/exception/users/__init__.py create mode 100644 backend/app/exception/users/signup_exeption.py create mode 100644 backend/app/exception/users/user_exception.py create mode 100644 backend/tests/api/__init__.py create mode 100644 backend/tests/api/routes/__init__.py create mode 100644 backend/tests/api/routes/users/__init__.py diff --git a/backend/app/exception/users/__init__.py b/backend/app/exception/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/exception/users/signup_exeption.py b/backend/app/exception/users/signup_exeption.py new file mode 100644 index 0000000..05f0aea --- /dev/null +++ b/backend/app/exception/users/signup_exeption.py @@ -0,0 +1,49 @@ +from .user_exception import UserException + +class UserSingUpException(UserException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpLoginIdMissningException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpPasswordMissningException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpNicknameMissningException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpEmailMissningException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpEmailMissningException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpInvalidLoginIdException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpInvalidPasswordException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpInvalidNicknameException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpInvalidEmailException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpLoginIdDuplicateException(UserSingUpException): + def __init__(self, message): + super().__init__(message) + +class UserSignUpNicknameDuplicateException(UserSingUpException): + def __init__(self, message): + super().__init__(message) diff --git a/backend/app/exception/users/user_exception.py b/backend/app/exception/users/user_exception.py new file mode 100644 index 0000000..dbbcee8 --- /dev/null +++ b/backend/app/exception/users/user_exception.py @@ -0,0 +1,6 @@ +class UserException(Exception): + def __init__(self, message): + super().__init__(message) + + def __str__(self): + return self.message diff --git a/backend/tests/api/__init__.py b/backend/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/__init__.py b/backend/tests/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/users/__init__.py b/backend/tests/api/routes/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/users/test_users.py b/backend/tests/api/routes/users/test_users.py index f120c46..008064e 100644 --- a/backend/tests/api/routes/users/test_users.py +++ b/backend/tests/api/routes/users/test_users.py @@ -1,36 +1,173 @@ import pytest +import requests -def test_회원가입_필수입력누락_오류(): +from .....app.exception.users.signup_exeption import ( + UserSignUpLoginIdMissningException, UserSignUpPasswordMissningException, + UserSignUpNicknameMissningException, UserSignUpEmailMissningException, + UserSignUpInvalidLoginIdException, UserSignUpInvalidPasswordException, + UserSignUpInvalidNicknameException, UserSignUpInvalidEmailException, + UserSignUpLoginIdDuplicateException, UserSignUpNicknameDuplicateException +) + +def test_회원가입_로그인아이디_누락_오류(): # given # POST /api/users + api_url = "http://localhost:8000/api/users" + data = { + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } # 입력: login_id, login_password, nickname, email + with pytest.raises(UserSignUpLoginIdMissningException): + requests.post(api_url, json=data) # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 - pass -def test_회원가입_필수입력검증_오류(): +def test_회원가입_로그인패스워드_누락_오류(): + # given + # POST /api/users + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + # 입력: login_id, login_password, nickname, email + with pytest.raises(UserSignUpPasswordMissningException): + requests.post(api_url, json=data) + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + +def test_회원가입_닉네임_누락_오류(): # given # POST /api/users + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'email': 'email@naver.com', + } # 입력: login_id, login_password, nickname, email + with pytest.raises(UserSignUpNicknameMissningException): + requests.post(api_url, json=data) # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 - pass + +def test_회원가입_이메일_누락_오류(): + # given + # POST /api/users + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + } + # 입력: login_id, login_password, nickname, email + with pytest.raises(UserSignUpEmailMissningException): + requests.post(api_url, json=data) + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + +def test_회원가입_로그인아이디_검증_오류(): + # given + # POST /api/users + # 입력: login_id, login_password, nickname, email + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + + with pytest.raises(UserSignUpInvalidLoginIdException): + requests.post(api_url, json=data) + +def test_회원가입_패스워드_검증_오류(): + # given + # POST /api/users + # 입력: login_id, login_password, nickname, email + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + with pytest.raises(UserSignUpInvalidPasswordException): + requests.post(api_url, json=data) + +def test_회원가입_닉네임_검증_오류(): + # given + # POST /api/users + # 입력: login_id, login_password, nickname, email + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + with pytest.raises(UserSignUpInvalidNicknameException): + requests.post(api_url, json=data) + +def test_회원가입_이메일_검증_오류(): + # given + # POST /api/users + # 입력: login_id, login_password, nickname, email + # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + with pytest.raises(UserSignUpInvalidEmailException): + requests.post(api_url, json=data) def test_회원가입_아이디중복_오류(): # GET /api/users?login_id={login_id} # - 400 Bad Request: # 409 Conflict: 이미 존재하는 아이디인 경우 + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + with pytest.raises(UserSignUpLoginIdDuplicateException): + requests.post(api_url, json=data) pass def test_회원가입_닉네임중복_오류(): # GET /api/users?nickname={nickname} # 409 Conflict: 이미 존재하는 닉네임인 경우 - pass + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + with pytest.raises(UserSignUpNicknameDuplicateException): + requests.post(api_url, json=data) def test_회원가입_정상(): # POST /api/users # 입력: login_id, login_password, nickname, email # 출력: 400 Bad Request: 필수 입력값이 누락되거나 입력값의 형태가 틀림 - - pass + api_url = "http://localhost:8000/api/users" + data = { + 'login_id': 'loginId123', + 'login_password': '12345678', + 'nickname': 'charlie', + 'email': 'email@naver.com', + } + + response = requests.post(api_url, json=data) + assert response.status_code == 200 def test_로그인_필수입력누락_오류(): # POST /api/users/auths From e20ce724bb605c950af869a0c898aee113cc38b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:37:38 +0900 Subject: [PATCH 035/187] =?UTF-8?q?Feat/5=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=ED=99=98=EA=B2=BD=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: .env파일 경로 찾기 구현 #5 * feat: .gitignore 추가 #5 * refactor: 환경변수파일 경로찾기 테스트 parametrize로 변경 #5 * test: 데이터소스 생성 정상 테스트 추가 #5 * feat: 데이터베이스 반환 기능 추가 #5 * feat: 데이터베이스 및 컬렉션 검증 및 반환 기능 구현 #5 --- .gitignore | 210 ++++++++++++++++++ backend/app/database/__init__.py | 0 backend/app/database/data_source.py | 64 ++++++ backend/app/database/dev.env | 4 + backend/app/database/prod.env | 4 + backend/app/exception/database/__init__.py | 0 .../exception/database/database_exception.py | 11 + backend/tests/assert_not_raises.py | 13 ++ backend/tests/database/__init__.py | 0 backend/tests/database/test_database.py | 79 +++++++ 10 files changed, 385 insertions(+) create mode 100644 .gitignore create mode 100644 backend/app/database/__init__.py create mode 100644 backend/app/database/data_source.py create mode 100644 backend/app/database/dev.env create mode 100644 backend/app/database/prod.env create mode 100644 backend/app/exception/database/__init__.py create mode 100644 backend/app/exception/database/database_exception.py create mode 100644 backend/tests/assert_not_raises.py create mode 100644 backend/tests/database/__init__.py create mode 100644 backend/tests/database/test_database.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..330f0e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,210 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,linux +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,linux + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,linux \ No newline at end of file diff --git a/backend/app/database/__init__.py b/backend/app/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/database/data_source.py b/backend/app/database/data_source.py new file mode 100644 index 0000000..43163fa --- /dev/null +++ b/backend/app/database/data_source.py @@ -0,0 +1,64 @@ +import os + +from pymongo import MongoClient +from pymongo.collection import Collection +from pymongo.database import Database +from dotenv import load_dotenv +from pydantic import BaseModel +from typing import Optional + +from ..exception.database.database_exception import ( + DatabaseNotFoundException, CollectionNotFoundException +) + +def env_file_of(env: str) -> str: + return os.path.join(os.path.dirname(os.path.abspath(__file__)), f"{env}.env") + +class DataSource(BaseModel): + host: str + port: str + database_name: str + client: Optional[object] = None + + def database(self) -> Database: + if self.client is None: + self._make_connection() + + self._validate_database() + return self.client[self.database_name] + + def collection_with_name_as(self, collection_name: str) -> Collection: + if self.client is None: + self._make_connection() + + self._validate_collection(collection_name) + return self.client[self.database_name][collection_name] + + def _make_connection(self) -> None: + url = f"mongodb://{self.host}:{self.port}/" + self.client = MongoClient(url) + + def _validate_database(self) -> None: + database_names = self.client.list_database_names() + if self.database_name not in database_names: + raise DatabaseNotFoundException(f"해당하는 데이터베이스가 존재하지 않습니다: {self.database_name}") + + def _validate_collection(self, collection_name) -> None: + collection_names = self.client[self.database_name].list_collection_names() + if collection_name not in collection_names: + raise CollectionNotFoundException(f"해당하는 컬렉션이 존재하지 않습니다: {collection_name}") + + +env_name = os.getenv('ENV', 'dev') +file_path = env_file_of(env_name) +assert os.path.exists(file_path), f"파일이 존재하지 않습니다: {file_path}" + +load_dotenv(file_path) + +data_env = { + 'host': os.getenv('HOST'), + 'port': os.getenv('PORT'), + 'database_name': os.getenv('DATABASE'), +} + +data_source = DataSource(**data_env) diff --git a/backend/app/database/dev.env b/backend/app/database/dev.env new file mode 100644 index 0000000..21fabab --- /dev/null +++ b/backend/app/database/dev.env @@ -0,0 +1,4 @@ +ENV=dev +HOST=localhost +PORT=27017 +DATABASE=dev diff --git a/backend/app/database/prod.env b/backend/app/database/prod.env new file mode 100644 index 0000000..7f55659 --- /dev/null +++ b/backend/app/database/prod.env @@ -0,0 +1,4 @@ +ENV=prod +HOST=10.0.6.6 +PORT=27017 +DATABASE=prod diff --git a/backend/app/exception/database/__init__.py b/backend/app/exception/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/exception/database/database_exception.py b/backend/app/exception/database/database_exception.py new file mode 100644 index 0000000..367724c --- /dev/null +++ b/backend/app/exception/database/database_exception.py @@ -0,0 +1,11 @@ +class DatabaseException(Exception): + def __init__(self, message): + super().__init__(message) + +class DatabaseNotFoundException(DatabaseException): + def __init__(self, message): + super().__init__(message) + +class CollectionNotFoundException(DatabaseException): + def __init__(self, message): + super().__init__(message) diff --git a/backend/tests/assert_not_raises.py b/backend/tests/assert_not_raises.py new file mode 100644 index 0000000..962e81d --- /dev/null +++ b/backend/tests/assert_not_raises.py @@ -0,0 +1,13 @@ +from contextlib import contextmanager + + +@contextmanager +def not_raises(ExpectedException): + try: + yield + + except ExpectedException as error: + raise AssertionError(f"Raised exception {error} when it should not!") + + except Exception as error: + raise AssertionError(f"An unexpected exception {error} raised.") \ No newline at end of file diff --git a/backend/tests/database/__init__.py b/backend/tests/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/database/test_database.py b/backend/tests/database/test_database.py new file mode 100644 index 0000000..b7e17a6 --- /dev/null +++ b/backend/tests/database/test_database.py @@ -0,0 +1,79 @@ +import pytest + +from ...app.database.data_source import DataSource, env_file_of +from ..assert_not_raises import not_raises + +@pytest.mark.parametrize("input, output", [ + ('dev', 'dev.env'), + ('prod', 'prod.env'), +]) +def test_환경변수파일_경로찾기_정상(input, output): + # given + env = input + + # when + filename = env_file_of(env).split('/')[-1] + + # then + assert filename == output + + +@pytest.mark.parametrize("host, port, database", [ + ('localhost', '27017', 'dev'), + ('10.0.6.6', '27017', 'prod'), +]) +def test_데이터소스_생성_정상(host, port, database): + # given + config = { + 'host': host, + 'port': port, + 'database_name': database + } + + # when, then + with not_raises(Exception): + DataSource(**config) + + +@pytest.mark.parametrize("host, port, database_name", [ + ('localhost', '27017', 'dev'), + ('10.0.6.6', '27017', 'prod'), +]) +def test_데이터베이스_반환_정상(host, port, database_name): + # given + config = { + 'host': host, + 'port': port, + 'database_name': database_name + } + datasource = DataSource(**config) + + # when + database = datasource.database() + + # then + assert database is not None + + +@pytest.mark.parametrize("host, port, database, collection_name" , [ + ('localhost', '27017', 'dev', 'users'), + ('localhost', '27017', 'dev', 'recipes'), + ('localhost', '27017', 'dev', 'ingredients'), + ('10.0.6.6', '27017', 'prod', 'users'), + ('10.0.6.6', '27017', 'prod', 'recipes'), + ('10.0.6.6', '27017', 'prod', 'ingredients'), +]) +def test_컬렉션_반환_정상(host, port, database, collection_name): + # given + config = { + 'host': host, + 'port': port, + 'database_name': database + } + datasource = DataSource(**config) + + # when + collection = datasource.collection_with_name_as(collection_name) + + # then + assert collection is not None From 9db9082c7dd540f38c98f9acae2b35d1eb821484 Mon Sep 17 00:00:00 2001 From: Sangwoo Shin <77473684+sangwoonoel@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:42:47 +0900 Subject: [PATCH 036/187] =?UTF-8?q?Feat/17=20=20commitizen=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: poetry 설정 # 17 * build: 컨벤션에 맞는 commit인지 체크하는 pre-commit hook 정의 # 17 * bump: version 0.1.0 → 0.2.0 --- backend/.pre-commit-config.yaml | 6 + backend/CHANGELOG.md | 7 + backend/poetry.lock | 1494 +++++++++++++++++++++++++++++++ backend/pyproject.toml | 31 + 4 files changed, 1538 insertions(+) create mode 100644 backend/.pre-commit-config.yaml create mode 100644 backend/CHANGELOG.md create mode 100644 backend/poetry.lock create mode 100644 backend/pyproject.toml diff --git a/backend/.pre-commit-config.yaml b/backend/.pre-commit-config.yaml new file mode 100644 index 0000000..1832358 --- /dev/null +++ b/backend/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/commitizen-tools/commitizen + rev: master + hooks: + - id: commitizen + stages: [commit-msg] \ No newline at end of file diff --git a/backend/CHANGELOG.md b/backend/CHANGELOG.md new file mode 100644 index 0000000..5427eaf --- /dev/null +++ b/backend/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.2.0 (2024-03-11) + +### Feat + +- test패키지 위치 변경 #3 +- frontend포함 패키지 구조 재설정 #3 +- 프로젝트 패키지 구조 설정 diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 0000000..a2225c8 --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,1494 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "3.7.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] + +[[package]] +name = "argcomplete" +version = "3.2.3" +description = "Bash tab completion for argparse" +optional = false +python-versions = ">=3.8" +files = [ + {file = "argcomplete-3.2.3-py3-none-any.whl", hash = "sha256:c12355e0494c76a2a7b73e3a59b09024ca0ba1e279fb9ed6c1b82d5b74b6a70c"}, + {file = "argcomplete-3.2.3.tar.gz", hash = "sha256:bf7900329262e481be5a15f56f19736b376df6f82ed27576fa893652c5de6c23"}, +] + +[package.extras] +test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "commitizen" +version = "3.18.0" +description = "Python commitizen client tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "commitizen-3.18.0-py3-none-any.whl", hash = "sha256:7e3272725216aa3049e258bb438e0097e07607712325f5ce71d9a0bdf7805370"}, + {file = "commitizen-3.18.0.tar.gz", hash = "sha256:42c1d29ba6a64ddbc6d9952f4bcc2ac7094956e27e47e96c931895e2659e6cee"}, +] + +[package.dependencies] +argcomplete = ">=1.12.1,<3.3" +charset-normalizer = ">=2.1.0,<4" +colorama = ">=0.4.1,<0.5.0" +decli = ">=0.6.0,<0.7.0" +importlib_metadata = ">=4.13,<8" +jinja2 = ">=2.10.3" +packaging = ">=19" +pyyaml = ">=3.08" +questionary = ">=2.0,<3.0" +termcolor = ">=1.1,<3" +tomlkit = ">=0.5.3,<1.0.0" + +[[package]] +name = "decli" +version = "0.6.1" +description = "Minimal, easy-to-use, declarative cli tool" +optional = false +python-versions = ">=3.7" +files = [ + {file = "decli-0.6.1-py3-none-any.whl", hash = "sha256:7815ac58617764e1a200d7cadac6315fcaacc24d727d182f9878dd6378ccf869"}, + {file = "decli-0.6.1.tar.gz", hash = "sha256:ed88ccb947701e8e5509b7945fda56e150e2ac74a69f25d47ac85ef30ab0c0f0"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "email-validator" +version = "2.1.1" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, + {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.105.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.105.0-py3-none-any.whl", hash = "sha256:f19ebf6fdc82a3281d10f2cb4774bdfa90238e3b40af3525a0c09fd08ad1c480"}, + {file = "fastapi-0.105.0.tar.gz", hash = "sha256:4d12838819aa52af244580675825e750ad67c9df4614f557a769606af902cf22"}, +] + +[package.dependencies] +anyio = ">=3.7.1,<4.0.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"all\""} +httpx = {version = ">=0.23.0", optional = true, markers = "extra == \"all\""} +itsdangerous = {version = ">=1.1.0", optional = true, markers = "extra == \"all\""} +jinja2 = {version = ">=2.11.2", optional = true, markers = "extra == \"all\""} +orjson = {version = ">=3.2.1", optional = true, markers = "extra == \"all\""} +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +pydantic-extra-types = {version = ">=2.0.0", optional = true, markers = "extra == \"all\""} +pydantic-settings = {version = ">=2.0.0", optional = true, markers = "extra == \"all\""} +python-multipart = {version = ">=0.0.5", optional = true, markers = "extra == \"all\""} +pyyaml = {version = ">=5.3.1", optional = true, markers = "extra == \"all\""} +starlette = ">=0.27.0,<0.28.0" +typing-extensions = ">=4.8.0" +ujson = {version = ">=4.0.1,<4.0.2 || >4.0.2,<4.1.0 || >4.1.0,<4.2.0 || >4.2.0,<4.3.0 || >4.3.0,<5.0.0 || >5.0.0,<5.1.0 || >5.1.0", optional = true, markers = "extra == \"all\""} +uvicorn = {version = ">=0.12.0", extras = ["standard"], optional = true, markers = "extra == \"all\""} + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.4" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.25.0)"] + +[[package]] +name = "httptools" +version = "0.6.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "identify" +version = "2.5.35" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.0.2" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, + {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "itsdangerous" +version = "2.1.2" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.7" +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "orjson" +version = "3.9.15" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.9.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d61f7ce4727a9fa7680cd6f3986b0e2c732639f46a5e0156e550e35258aa313a"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"}, + {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"}, + {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"}, + {file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"}, + {file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"}, + {file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"}, + {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"}, + {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"}, + {file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"}, + {file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"}, + {file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"}, + {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"}, + {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"}, + {file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"}, + {file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"}, + {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"}, + {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"}, + {file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"}, + {file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"}, + {file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"}, + {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"}, + {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"}, + {file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"}, + {file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"}, + {file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.6.2" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, + {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "pydantic" +version = "2.6.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-extra-types" +version = "2.6.0" +description = "Extra Pydantic types." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_extra_types-2.6.0-py3-none-any.whl", hash = "sha256:d291d521c2e2bf2e6f11971caf8d639518124ae26a76d2e712599e98c4ef2b2b"}, + {file = "pydantic_extra_types-2.6.0.tar.gz", hash = "sha256:e9a93cfb245158462acb76621785219f80ad112303a0a7784d2ada65e6ed6cba"}, +] + +[package.dependencies] +pydantic = ">=2.5.2" + +[package.extras] +all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "python-ulid (>=1,<2)", "python-ulid (>=1,<3)"] + +[[package]] +name = "pydantic-settings" +version = "2.2.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, +] + +[package.dependencies] +pydantic = ">=2.3.0" +python-dotenv = ">=0.21.0" + +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.9" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] + +[package.extras] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "questionary" +version = "2.0.1" +description = "Python library to build pretty command line user prompts ⭐️" +optional = false +python-versions = ">=3.8" +files = [ + {file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"}, + {file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"}, +] + +[package.dependencies] +prompt_toolkit = ">=2.0,<=3.0.36" + +[[package]] +name = "setuptools" +version = "69.1.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.27.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.7" +files = [ + {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, + {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + +[[package]] +name = "termcolor" +version = "2.4.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.8" +files = [ + {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, + {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.4" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, + {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, +] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "ujson" +version = "5.9.0" +description = "Ultra fast JSON encoder and decoder for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "ujson-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab71bf27b002eaf7d047c54a68e60230fbd5cd9da60de7ca0aa87d0bccead8fa"}, + {file = "ujson-5.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a365eac66f5aa7a7fdf57e5066ada6226700884fc7dce2ba5483538bc16c8c5"}, + {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e015122b337858dba5a3dc3533af2a8fc0410ee9e2374092f6a5b88b182e9fcc"}, + {file = "ujson-5.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:779a2a88c53039bebfbccca934430dabb5c62cc179e09a9c27a322023f363e0d"}, + {file = "ujson-5.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10ca3c41e80509fd9805f7c149068fa8dbee18872bbdc03d7cca928926a358d5"}, + {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a566e465cb2fcfdf040c2447b7dd9718799d0d90134b37a20dff1e27c0e9096"}, + {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f833c529e922577226a05bc25b6a8b3eb6c4fb155b72dd88d33de99d53113124"}, + {file = "ujson-5.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b68a0caab33f359b4cbbc10065c88e3758c9f73a11a65a91f024b2e7a1257106"}, + {file = "ujson-5.9.0-cp310-cp310-win32.whl", hash = "sha256:7cc7e605d2aa6ae6b7321c3ae250d2e050f06082e71ab1a4200b4ae64d25863c"}, + {file = "ujson-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6d3f10eb8ccba4316a6b5465b705ed70a06011c6f82418b59278fbc919bef6f"}, + {file = "ujson-5.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b23bbb46334ce51ddb5dded60c662fbf7bb74a37b8f87221c5b0fec1ec6454b"}, + {file = "ujson-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6974b3a7c17bbf829e6c3bfdc5823c67922e44ff169851a755eab79a3dd31ec0"}, + {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5964ea916edfe24af1f4cc68488448fbb1ec27a3ddcddc2b236da575c12c8ae"}, + {file = "ujson-5.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ba7cac47dd65ff88571eceeff48bf30ed5eb9c67b34b88cb22869b7aa19600d"}, + {file = "ujson-5.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bbd91a151a8f3358c29355a491e915eb203f607267a25e6ab10531b3b157c5e"}, + {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:829a69d451a49c0de14a9fecb2a2d544a9b2c884c2b542adb243b683a6f15908"}, + {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a807ae73c46ad5db161a7e883eec0fbe1bebc6a54890152ccc63072c4884823b"}, + {file = "ujson-5.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8fc2aa18b13d97b3c8ccecdf1a3c405f411a6e96adeee94233058c44ff92617d"}, + {file = "ujson-5.9.0-cp311-cp311-win32.whl", hash = "sha256:70e06849dfeb2548be48fdd3ceb53300640bc8100c379d6e19d78045e9c26120"}, + {file = "ujson-5.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:7309d063cd392811acc49b5016728a5e1b46ab9907d321ebbe1c2156bc3c0b99"}, + {file = "ujson-5.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:20509a8c9f775b3a511e308bbe0b72897ba6b800767a7c90c5cca59d20d7c42c"}, + {file = "ujson-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b28407cfe315bd1b34f1ebe65d3bd735d6b36d409b334100be8cdffae2177b2f"}, + {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d302bd17989b6bd90d49bade66943c78f9e3670407dbc53ebcf61271cadc399"}, + {file = "ujson-5.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f21315f51e0db8ee245e33a649dd2d9dce0594522de6f278d62f15f998e050e"}, + {file = "ujson-5.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5635b78b636a54a86fdbf6f027e461aa6c6b948363bdf8d4fbb56a42b7388320"}, + {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82b5a56609f1235d72835ee109163c7041b30920d70fe7dac9176c64df87c164"}, + {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5ca35f484622fd208f55041b042d9d94f3b2c9c5add4e9af5ee9946d2d30db01"}, + {file = "ujson-5.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:829b824953ebad76d46e4ae709e940bb229e8999e40881338b3cc94c771b876c"}, + {file = "ujson-5.9.0-cp312-cp312-win32.whl", hash = "sha256:25fa46e4ff0a2deecbcf7100af3a5d70090b461906f2299506485ff31d9ec437"}, + {file = "ujson-5.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:60718f1720a61560618eff3b56fd517d107518d3c0160ca7a5a66ac949c6cf1c"}, + {file = "ujson-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d581db9db9e41d8ea0b2705c90518ba623cbdc74f8d644d7eb0d107be0d85d9c"}, + {file = "ujson-5.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff741a5b4be2d08fceaab681c9d4bc89abf3c9db600ab435e20b9b6d4dfef12e"}, + {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdcb02cabcb1e44381221840a7af04433c1dc3297af76fde924a50c3054c708c"}, + {file = "ujson-5.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e208d3bf02c6963e6ef7324dadf1d73239fb7008491fdf523208f60be6437402"}, + {file = "ujson-5.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4b3917296630a075e04d3d07601ce2a176479c23af838b6cf90a2d6b39b0d95"}, + {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0c4d6adb2c7bb9eb7c71ad6f6f612e13b264942e841f8cc3314a21a289a76c4e"}, + {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0b159efece9ab5c01f70b9d10bbb77241ce111a45bc8d21a44c219a2aec8ddfd"}, + {file = "ujson-5.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0cb4a7814940ddd6619bdce6be637a4b37a8c4760de9373bac54bb7b229698b"}, + {file = "ujson-5.9.0-cp38-cp38-win32.whl", hash = "sha256:dc80f0f5abf33bd7099f7ac94ab1206730a3c0a2d17549911ed2cb6b7aa36d2d"}, + {file = "ujson-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:506a45e5fcbb2d46f1a51fead991c39529fc3737c0f5d47c9b4a1d762578fc30"}, + {file = "ujson-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0fd2eba664a22447102062814bd13e63c6130540222c0aa620701dd01f4be81"}, + {file = "ujson-5.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bdf7fc21a03bafe4ba208dafa84ae38e04e5d36c0e1c746726edf5392e9f9f36"}, + {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f909bc08ce01f122fd9c24bc6f9876aa087188dfaf3c4116fe6e4daf7e194f"}, + {file = "ujson-5.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd4ea86c2afd41429751d22a3ccd03311c067bd6aeee2d054f83f97e41e11d8f"}, + {file = "ujson-5.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63fb2e6599d96fdffdb553af0ed3f76b85fda63281063f1cb5b1141a6fcd0617"}, + {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:32bba5870c8fa2a97f4a68f6401038d3f1922e66c34280d710af00b14a3ca562"}, + {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:37ef92e42535a81bf72179d0e252c9af42a4ed966dc6be6967ebfb929a87bc60"}, + {file = "ujson-5.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f69f16b8f1c69da00e38dc5f2d08a86b0e781d0ad3e4cc6a13ea033a439c4844"}, + {file = "ujson-5.9.0-cp39-cp39-win32.whl", hash = "sha256:3382a3ce0ccc0558b1c1668950008cece9bf463ebb17463ebf6a8bfc060dae34"}, + {file = "ujson-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:6adef377ed583477cf005b58c3025051b5faa6b8cc25876e594afbb772578f21"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ffdfebd819f492e48e4f31c97cb593b9c1a8251933d8f8972e81697f00326ff1"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4eec2ddc046360d087cf35659c7ba0cbd101f32035e19047013162274e71fcf"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbb90aa5c23cb3d4b803c12aa220d26778c31b6e4b7a13a1f49971f6c7d088e"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba0823cb70866f0d6a4ad48d998dd338dce7314598721bc1b7986d054d782dfd"}, + {file = "ujson-5.9.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4e35d7885ed612feb6b3dd1b7de28e89baaba4011ecdf995e88be9ac614765e9"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b048aa93eace8571eedbd67b3766623e7f0acbf08ee291bef7d8106210432427"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:323279e68c195110ef85cbe5edce885219e3d4a48705448720ad925d88c9f851"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ac92d86ff34296f881e12aa955f7014d276895e0e4e868ba7fddebbde38e378"}, + {file = "ujson-5.9.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6eecbd09b316cea1fd929b1e25f70382917542ab11b692cb46ec9b0a26c7427f"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:473fb8dff1d58f49912323d7cb0859df5585cfc932e4b9c053bf8cf7f2d7c5c4"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f91719c6abafe429c1a144cfe27883eace9fb1c09a9c5ef1bcb3ae80a3076a4e"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b1c0991c4fe256f5fdb19758f7eac7f47caac29a6c57d0de16a19048eb86bad"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ea0f55a1396708e564595aaa6696c0d8af532340f477162ff6927ecc46e21"}, + {file = "ujson-5.9.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:07e0cfdde5fd91f54cd2d7ffb3482c8ff1bf558abf32a8b953a5d169575ae1cd"}, + {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"}, +] + +[[package]] +name = "uvicorn" +version = "0.28.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, + {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.19.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "virtualenv" +version = "20.25.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchfiles" +version = "0.21.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, + {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, + {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, + {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, + {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, + {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, + {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, + {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, + {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, + {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, + {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, + {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, + {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, + {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[[package]] +name = "zipp" +version = "3.17.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "021f3e4cdf9b94c43724ee684a3621becdbb720e9cef6187d86c07768e8c1ea4" diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..d46357c --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,31 @@ +[tool.poetry] +name = "finalproject" +version = "0.2.0" +description = "" +authors = ["sangwoonoel "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.9" +fastapi = {extras = ["all"], version = "^0.105.0"} +pydantic-settings = "^2.1.0" +loguru = "^0.7.2" +httpx = "^0.27.0" +pytest = "^8.0.2" + + +[tool.poetry.group.dev.dependencies] +commitizen = "^3.18.0" +pre-commit = "^3.6.2" + + +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "pep440" +version_provider = "poetry" +update_changelog_on_bump = true +major_version_zero = true +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 11d5c14e42148ced280e18fb1b1bde234f1c52b3 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Tue, 12 Mar 2024 15:07:36 +0900 Subject: [PATCH 037/187] =?UTF-8?q?build:=20poetry=EC=97=90=20pymongo=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- backend/poetry.lock | 105 ++++++++++++++++++++++++++++++++++++++++- backend/pyproject.toml | 1 + 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/backend/poetry.lock b/backend/poetry.lock index a2225c8..f5698c2 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -900,6 +900,109 @@ python-dotenv = ">=0.21.0" toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] +[[package]] +name = "pymongo" +version = "4.6.2" +description = "Python driver for MongoDB " +optional = false +python-versions = ">=3.7" +files = [ + {file = "pymongo-4.6.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7640d176ee5b0afec76a1bda3684995cb731b2af7fcfd7c7ef8dc271c5d689af"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:4e2129ec8f72806751b621470ac5d26aaa18fae4194796621508fa0e6068278a"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c43205e85cbcbdf03cff62ad8f50426dd9d20134a915cfb626d805bab89a1844"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:91ddf95cedca12f115fbc5f442b841e81197d85aa3cc30b82aee3635a5208af2"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:0fbdbf2fba1b4f5f1522e9f11e21c306e095b59a83340a69e908f8ed9b450070"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:097791d5a8d44e2444e0c8c4d6e14570ac11e22bcb833808885a5db081c3dc2a"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:e0b208ebec3b47ee78a5c836e2e885e8c1e10f8ffd101aaec3d63997a4bdcd04"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1849fd6f1917b4dc5dbf744b2f18e41e0538d08dd8e9ba9efa811c5149d665a3"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa0bbbfbd1f8ebbd5facaa10f9f333b20027b240af012748555148943616fdf3"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4522ad69a4ab0e1b46a8367d62ad3865b8cd54cf77518c157631dac1fdc97584"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397949a9cc85e4a1452f80b7f7f2175d557237177120954eff00bf79553e89d3"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d511db310f43222bc58d811037b176b4b88dc2b4617478c5ef01fea404f8601"}, + {file = "pymongo-4.6.2-cp310-cp310-win32.whl", hash = "sha256:991e406db5da4d89fb220a94d8caaf974ffe14ce6b095957bae9273c609784a0"}, + {file = "pymongo-4.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:94637941fe343000f728e28d3fe04f1f52aec6376b67b85583026ff8dab2a0e0"}, + {file = "pymongo-4.6.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84593447a5c5fe7a59ba86b72c2c89d813fbac71c07757acdf162fbfd5d005b9"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aebddb2ec2128d5fc2fe3aee6319afef8697e0374f8a1fcca3449d6f625e7b4"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f706c1a644ed33eaea91df0a8fb687ce572b53eeb4ff9b89270cb0247e5d0e1"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c422e6b08fa370ed9d8670c67e78d01f50d6517cec4522aa8627014dfa38b6"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d002ae456a15b1d790a78bb84f87af21af1cb716a63efb2c446ab6bcbbc48ca"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f86ba0c781b497a3c9c886765d7b6402a0e3ae079dd517365044c89cd7abb06"}, + {file = "pymongo-4.6.2-cp311-cp311-win32.whl", hash = "sha256:ac20dd0c7b42555837c86f5ea46505f35af20a08b9cf5770cd1834288d8bd1b4"}, + {file = "pymongo-4.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:e78af59fd0eb262c2a5f7c7d7e3b95e8596a75480d31087ca5f02f2d4c6acd19"}, + {file = "pymongo-4.6.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6125f73503407792c8b3f80165f8ab88a4e448d7d9234c762681a4d0b446fcb4"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba052446a14bd714ec83ca4e77d0d97904f33cd046d7bb60712a6be25eb31dbb"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b65433c90e07dc252b4a55dfd885ca0df94b1cf77c5b8709953ec1983aadc03"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2160d9c8cd20ce1f76a893f0daf7c0d38af093f36f1b5c9f3dcf3e08f7142814"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f251f287e6d42daa3654b686ce1fcb6d74bf13b3907c3ae25954978c70f2cd4"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d227a60b00925dd3aeae4675575af89c661a8e89a1f7d1677e57eba4a3693c"}, + {file = "pymongo-4.6.2-cp312-cp312-win32.whl", hash = "sha256:311794ef3ccae374aaef95792c36b0e5c06e8d5cf04a1bdb1b2bf14619ac881f"}, + {file = "pymongo-4.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:f673b64a0884edcc56073bda0b363428dc1bf4eb1b5e7d0b689f7ec6173edad6"}, + {file = "pymongo-4.6.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:fe010154dfa9e428bd2fb3e9325eff2216ab20a69ccbd6b5cac6785ca2989161"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1f5f4cd2969197e25b67e24d5b8aa2452d381861d2791d06c493eaa0b9c9fcfe"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c9519c9d341983f3a1bd19628fecb1d72a48d8666cf344549879f2e63f54463b"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c68bf4a399e37798f1b5aa4f6c02886188ef465f4ac0b305a607b7579413e366"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a509db602462eb736666989739215b4b7d8f4bb8ac31d0bffd4be9eae96c63ef"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:362a5adf6f3f938a8ff220a4c4aaa93e84ef932a409abecd837c617d17a5990f"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:ee30a9d4c27a88042d0636aca0275788af09cc237ae365cd6ebb34524bddb9cc"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:477914e13501bb1d4608339ee5bb618be056d2d0e7267727623516cfa902e652"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd343ca44982d480f1e39372c48e8e263fc6f32e9af2be456298f146a3db715"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3797e0a628534e07a36544d2bfa69e251a578c6d013e975e9e3ed2ac41f2d95"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97d81d357e1a2a248b3494d52ebc8bf15d223ee89d59ee63becc434e07438a24"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed694c0d1977cb54281cb808bc2b247c17fb64b678a6352d3b77eb678ebe1bd9"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ceaaff4b812ae368cf9774989dea81b9bbb71e5bed666feca6a9f3087c03e49"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7dd63f7c2b3727541f7f37d0fb78d9942eb12a866180fbeb898714420aad74e2"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e571434633f99a81e081738721bb38e697345281ed2f79c2f290f809ba3fbb2f"}, + {file = "pymongo-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:3e9f6e2f3da0a6af854a3e959a6962b5f8b43bbb8113cd0bff0421c5059b3106"}, + {file = "pymongo-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3a5280f496297537301e78bde250c96fadf4945e7b2c397d8bb8921861dd236d"}, + {file = "pymongo-4.6.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:5f6bcd2d012d82d25191a911a239fd05a8a72e8c5a7d81d056c0f3520cad14d1"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4fa30494601a6271a8b416554bd7cde7b2a848230f0ec03e3f08d84565b4bf8c"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bea62f03a50f363265a7a651b4e2a4429b4f138c1864b2d83d4bf6f9851994be"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b2d445f1cf147331947cc35ec10342f898329f29dd1947a3f8aeaf7e0e6878d1"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:5db133d6ec7a4f7fc7e2bd098e4df23d7ad949f7be47b27b515c9fb9301c61e4"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:9eec7140cf7513aa770ea51505d312000c7416626a828de24318fdcc9ac3214c"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:5379ca6fd325387a34cda440aec2bd031b5ef0b0aa2e23b4981945cff1dab84c"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:579508536113dbd4c56e4738955a18847e8a6c41bf3c0b4ab18b51d81a6b7be8"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bae553ca39ed52db099d76acd5e8566096064dc7614c34c9359bb239ec4081"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0257e0eebb50f242ca28a92ef195889a6ad03dcdde5bf1c7ab9f38b7e810801"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbafe3a1df21eeadb003c38fc02c1abf567648b6477ec50c4a3c042dca205371"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaecfafb407feb6f562c7f2f5b91f22bfacba6dd739116b1912788cff7124c4a"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e942945e9112075a84d2e2d6e0d0c98833cdcdfe48eb8952b917f996025c7ffa"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f7b98f8d2cf3eeebde738d080ae9b4276d7250912d9751046a9ac1efc9b1ce2"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8110b78fc4b37dced85081d56795ecbee6a7937966e918e05e33a3900e8ea07d"}, + {file = "pymongo-4.6.2-cp38-cp38-win32.whl", hash = "sha256:df813f0c2c02281720ccce225edf39dc37855bf72cdfde6f789a1d1cf32ffb4b"}, + {file = "pymongo-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:64ec3e2dcab9af61bdbfcb1dd863c70d1b0c220b8e8ac11df8b57f80ee0402b3"}, + {file = "pymongo-4.6.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bff601fbfcecd2166d9a2b70777c2985cb9689e2befb3278d91f7f93a0456cae"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f1febca6f79e91feafc572906871805bd9c271b6a2d98a8bb5499b6ace0befed"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d788cb5cc947d78934be26eef1623c78cec3729dc93a30c23f049b361aa6d835"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5c2f258489de12a65b81e1b803a531ee8cf633fa416ae84de65cd5f82d2ceb37"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:fb24abcd50501b25d33a074c1790a1389b6460d2509e4b240d03fd2e5c79f463"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:4d982c6db1da7cf3018183891883660ad085de97f21490d314385373f775915b"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:b2dd8c874927a27995f64a3b44c890e8a944c98dec1ba79eab50e07f1e3f801b"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:4993593de44c741d1e9f230f221fe623179f500765f9855936e4ff6f33571bad"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658f6c028edaeb02761ebcaca8d44d519c22594b2a51dcbc9bd2432aa93319e3"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68109c13176749fbbbbbdb94dd4a58dcc604db6ea43ee300b2602154aebdd55f"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:707d28a822b918acf941cff590affaddb42a5d640614d71367c8956623a80cbc"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f251db26c239aec2a4d57fbe869e0a27b7f6b5384ec6bf54aeb4a6a5e7408234"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57c05f2e310701fc17ae358caafd99b1830014e316f0242d13ab6c01db0ab1c2"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b575fbe6396bbf21e4d0e5fd2e3cdb656dc90c930b6c5532192e9a89814f72d"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca5877754f3fa6e4fe5aacf5c404575f04c2d9efc8d22ed39576ed9098d555c8"}, + {file = "pymongo-4.6.2-cp39-cp39-win32.whl", hash = "sha256:8caa73fb19070008e851a589b744aaa38edd1366e2487284c61158c77fdf72af"}, + {file = "pymongo-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:3e03c732cb64b96849310e1d8688fb70d75e2571385485bf2f1e7ad1d309fa53"}, + {file = "pymongo-4.6.2.tar.gz", hash = "sha256:ab7d01ac832a1663dad592ccbd92bb0f0775bc8f98a1923c5e1a7d7fead495af"}, +] + +[package.dependencies] +dnspython = ">=1.16.0,<3.0.0" + +[package.extras] +aws = ["pymongo-auth-aws (<2.0.0)"] +encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] +gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] +ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +test = ["pytest (>=7)"] +zstd = ["zstandard"] + [[package]] name = "pytest" version = "8.1.1" @@ -1491,4 +1594,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "021f3e4cdf9b94c43724ee684a3621becdbb720e9cef6187d86c07768e8c1ea4" +content-hash = "4af0ff2503b6d87b2d58f0e73b6df658fdf4ede39b4e01e4d141f3d4e25186e2" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d46357c..dbf8699 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -12,6 +12,7 @@ pydantic-settings = "^2.1.0" loguru = "^0.7.2" httpx = "^0.27.0" pytest = "^8.0.2" +pymongo = "^4.6.2" [tool.poetry.group.dev.dependencies] From 425cb3c6a8eb8b5ac8c8fc419a6c8008cfe4e61b Mon Sep 17 00:00:00 2001 From: twndus Date: Tue, 12 Mar 2024 15:18:36 +0900 Subject: [PATCH 038/187] =?UTF-8?q?refactor(Home,-pages):=20Mock=20API=20?= =?UTF-8?q?=EB=A5=BC=20=ED=98=B8=EC=B6=9C=ED=95=98=EC=97=AC=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EB=A5=BC=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=EB=A9=80=ED=8B=B0=EC=84=B8=EC=85=98=20=EB=B0=8F?= =?UTF-8?q?=20st=20=EB=A9=80=ED=8B=B0=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC?= =?UTF-8?q?=20=ED=86=B5=ED=95=B4=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6 #7 #9 --- frontend/Home.py | 18 +-- frontend/pages/RecommendationHistoryPage.py | 123 ++++++------------ frontend/pages/RecommendationPage.py | 99 +++++++-------- frontend/pages/ResultPage1.py | 132 -------------------- frontend/utils.py | 117 +++++++++-------- 5 files changed, 153 insertions(+), 336 deletions(-) delete mode 100644 frontend/pages/ResultPage1.py diff --git a/frontend/Home.py b/frontend/Home.py index 9e10901..a2367e5 100644 --- a/frontend/Home.py +++ b/frontend/Home.py @@ -1,14 +1,16 @@ # Temporal Entrypoint import streamlit as st -from utils import menu_tab +from utils import page_header -# page labeling -st.set_page_config( - page_title="Entrypoint", - page_icon="🛒", -) +# define session-page +def home(): + page_header(False, None) +# 세션 초기화 +if 'page_info' not in st.session_state: + st.session_state['page_info'] = 'home' +st.session_state['page_info'] = 'home' -# 상단 메뉴 -menu_tab(login=True, user='Judy') +# 이전 추천 목록 조회 +home() diff --git a/frontend/pages/RecommendationHistoryPage.py b/frontend/pages/RecommendationHistoryPage.py index 86e8877..61dc0da 100644 --- a/frontend/pages/RecommendationHistoryPage.py +++ b/frontend/pages/RecommendationHistoryPage.py @@ -3,104 +3,59 @@ import numpy as np import time -from utils import menu_tab import requests +from utils import page_header, get_response, patch_feedback + user = '주디' user_id = '1' page_num = '1' -# page labeling -st.set_page_config( - page_title="RecommendationHistoryPage", -) - -# 상단바 -menu_tab(login=True, user='Judy') - -# RecommendationHistoryByPage request -response = requests.get(f"https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes?page={page_num}") -if response.status_code == 200: - data = response.json()['food_list'] -else: - print(f'status code: {response.status_code}') - data = None +def recommendation_history_page(): -# 페이지 구성 -container = st.container(border=True) + # 앱 헤더 + page_header(False, None) -with container: + url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/recommended?page={page_num}" + user_id, page_num = 1,1 + formatted_url = url.format(user_id=user_id, page_num=page_num) + data = get_response(formatted_url) - st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) - - sub_container = st.container(border=False) + # 페이지 구성 + container = st.container(border=True) - with sub_container: - st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) - st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) + with container: + st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) + + sub_container = st.container(border=False) + with sub_container: + st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) + st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) -# foods = [ -# '어묵김말이', -# '두부새우전', -# '알밥', -# '현미호두죽', -# ] -# -# img_urls = [ -# 'https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg', -# 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg', -# 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg', -# 'https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg', -# ] -# -# recipe_urls = [ -# 'https://www.10000recipe.com/recipe/128671', -# 'https://www.10000recipe.com/recipe/128892', -# 'https://www.10000recipe.com/recipe/128932', -# 'https://www.10000recipe.com/recipe/131871', -# ] + recipe_list = data['recipe_list'] + for row in range(int(len(recipe_list)/4)): + cols = st.columns(4) - feedback = [ True, False, False, True ] - - #for url in range(int(len(img_urls)/4)): - for row in range(int(len(data)/4)): - cols = st.columns(4) + for i in range(4): + if row == len(recipe_list)//4: i = len(recipe_list)%4 - for i in range(4): - if row == len(data)//4: i = len(data)%4 - with cols[i]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - sub_cols = st.columns([3,1]) - with sub_cols[0]: - st.markdown(f'

{data[i]["food_name"]}

', unsafe_allow_html=True) + with cols[i]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + sub_cols = st.columns([3,1]) + with sub_cols[0]: + st.markdown(f'

{recipe_list[i]["recipe_name"]}

', unsafe_allow_html=True) - if feedback[i]: - btn_label = '❤️' - else: - btn_label = '🩶' - with sub_cols[-1]: - st.markdown(f'', unsafe_allow_html=True) - # POST /api/users/{user_id}/foods - # inputs: user_id, List[food_id] + icon_mapper = lambda cooked: '❤️' if cooked else '🩶' + cooked = recipe_list[i]['recipe_id'] in data['user_feedback'] + with sub_cols[-1]: + st.button( + icon_mapper(cooked), + on_click=patch_feedback, + key=f'feedback_{i}', + args=(user_id, recipe_list[i]['recipe_id'], cooked)) -st.markdown(""" - - """, unsafe_allow_html=True) +st.session_state['page_info'] = 'recommend_history' +recommendation_history_page() diff --git a/frontend/pages/RecommendationPage.py b/frontend/pages/RecommendationPage.py index 1f393ee..ee3e4b8 100644 --- a/frontend/pages/RecommendationPage.py +++ b/frontend/pages/RecommendationPage.py @@ -3,56 +3,49 @@ import numpy as np import time -from utils import menu_tab - -user = '주디' - -# page labeling -st.set_page_config( - page_title="Hello", - page_icon="👋", -) - -# 상단바 -menu_tab(login=True, user='Judy') - -# 페이지 구성 -container = st.container(border=True) - -with container: - st.markdown("

이번 주 장바구니 만들기

", unsafe_allow_html=True) - st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) - st.markdown("
예산을 정해주세요.
", unsafe_allow_html=True) - - cols = st.columns([1,5,1]) - with cols[1]: - price = st.slider( - label='', - min_value=10000, - max_value=1000000, - value=50000, - step=5000 - ) - - cols = st.columns(5) - with cols[2]: - st.write("예산: ", price, '원') - - - with cols[1]: - button1 = st.button("이전 장바구니 보기") - - with cols[3]: - button2 = st.button("다음 장바구니 보기", type="primary") - - st.markdown( - """""", - unsafe_allow_html=True, - ) +from utils import page_header, basket_feedback +from pages.ResultPage2 import result_page_2 + +def recommendation_page(): + + # 앱 헤더 + page_header(False, None) + + # 페이지 구성 + container = st.container(border=True) + + with container: + st.markdown("

이번 주 장바구니 만들기

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + st.markdown("
예산을 정해주세요.
", unsafe_allow_html=True) + + cols = st.columns([1,5,1]) + + with cols[1]: + + price = st.slider( + label='', min_value=10000, max_value=1000000, value=50000, step=5000 + ) + + cols = st.columns(5) + + with cols[2]: + st.write("예산: ", price, '원') + + + cols = st.columns(3) + + with cols[1]: + button2 = st.button("장바구니 추천받기", type="primary") + if button2: + st.session_state['page_info'] = 'result_page_1' + + +# 이전 추천 목록 조회 +if 'page_info' not in st.session_state: + st.session_state['page_info'] = 'recommend' + +if st.session_state['page_info'] == 'result_page_1': + result_page_2() +else: + recommendation_page() diff --git a/frontend/pages/ResultPage1.py b/frontend/pages/ResultPage1.py deleted file mode 100644 index 2802c82..0000000 --- a/frontend/pages/ResultPage1.py +++ /dev/null @@ -1,132 +0,0 @@ -import streamlit as st -import pandas as pd -import numpy as np -import time - -from utils import menu_tab, basket_feedback - -user = '주디' -ingredients = [ - {'ingredient_name': '브로콜리', - 'amount': 1, 'unit': 'kg', - 'price': 4680, - 'img_url': 'https://health.chosun.com/site/data/img_dir/2024/01/19/2024011902009_0.jpg', - 'market_url': 'https://www.coupang.com/vp/products/4874444452?itemId=6339533080&vendorItemId=73634892616&pickType=COU_PICK&q=%EB%B8%8C%EB%A1%9C%EC%BD%9C%EB%A6%AC&itemsCount=36&searchId=891d0b69dc8f452daf392e3db2482732&rank=1&isAddedCart='}, - {'ingredient_name': '초고추장', - 'amount': 500, 'unit': 'g', - 'price': 5000, - 'img_url': 'https://image7.coupangcdn.com/image/retail/images/4810991441045098-31358d86-eff6-45f4-8ed6-f36b642e8944.jpg', - 'market_url': 'https://www.coupang.com/vp/products/6974484284?itemId=17019959259&vendorItemId=3000138402&q=%EC%B4%88%EA%B3%A0%EC%B6%94%EC%9E%A5&itemsCount=36&searchId=d5538b6e86d04be3938c98ef1655df85&rank=1&isAddedCart='}, - ] - -# page labeling -st.set_page_config( - page_title="ResultPage-1", -) - -# 상단바 -menu_tab(login=True, user='Judy') - -# 페이지 구성 -container = st.container(border=True) - -with container: - - st.markdown("

새로운 장바구니를 추천받았어요!

", unsafe_allow_html=True) - st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) - - st.divider() - - st.markdown("

추천 장바구니

", unsafe_allow_html=True) - - total_price = 0 - - for ingredient in ingredients: - sub_container = st.container(border=True) - - with sub_container: - - cols = st.columns(5) - with cols[0]: - st.image(ingredient['img_url']) - with cols[1]: - st.write(ingredient['ingredient_name']) - st.write(ingredient['amount'], ingredient['unit']) - - with cols[-1]: - st.link_button('구매', ingredient['market_url'], type='primary') - - total_price += ingredient['price'] - - st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) - - st.divider() - - st.markdown("

이 장바구니로 만들 수 있는 음식 레시피

", unsafe_allow_html=True) - - foods = [ - '어묵김말이', - '두부새우전', - '알밥', - '현미호두죽', - ] - - img_urls = [ - 'https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg', - 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg', - 'https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg', - 'https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg', - ] - - recipe_urls = [ - 'https://www.10000recipe.com/recipe/128671', - 'https://www.10000recipe.com/recipe/128892', - 'https://www.10000recipe.com/recipe/128932', - 'https://www.10000recipe.com/recipe/131871', - ] - - feedback = [ True, False, False, True ] - - for url in range(int(len(img_urls)/4)): - cols = st.columns(4) - - for i in range(4): - with cols[i]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - sub_cols = st.columns([3,1]) - with sub_cols[0]: - st.markdown(f'

{foods[i]}

', unsafe_allow_html=True) - - if feedback[i]: - btn_label = '❤️' - else: - btn_label = '🩶' - with sub_cols[-1]: - st.markdown(f'', unsafe_allow_html=True) - # POST /api/users/{user_id}/foods - # inputs: user_id, List[food_id] - - st.text("\n\n") - - basket_feedback() - - -st.markdown(""" - - """, unsafe_allow_html=True) diff --git a/frontend/utils.py b/frontend/utils.py index c31ca61..d0fd51d 100644 --- a/frontend/utils.py +++ b/frontend/utils.py @@ -1,67 +1,27 @@ import streamlit as st +import requests -def menu_tab(login=False, user=None): - # layout - cols = st.columns([1,2]) +def set_logout(): + st.session_state['key'] = None - with cols[0]: - st.markdown('', unsafe_allow_html=True) +def set_login(): + st.session_state['key'] = 'session-key' - with cols[1]: - cols2 = st.columns([5,5,4,4]) - with cols2[0]: - st.markdown('', unsafe_allow_html=True) - with cols2[1]: - st.markdown('', unsafe_allow_html=True) - with cols2[2]: - st.markdown('', unsafe_allow_html=True) - with cols2[3]: - if login: - st.markdown(f'', unsafe_allow_html=True) - else: - st.markdown(f'', unsafe_allow_html=True) +def login_button(login, user): + if login: + login_button = st.button(f"{user}님 | 로그아웃", on_click=set_logout) + else: + login_button = st.button(f"회원가입 | 로그인", on_click=set_login) - st.markdown( - """ - - """, - unsafe_allow_html=True, - ) + return login_button + +def page_header(login, user): + cols = st.columns([8,3]) + with cols[0]: + st.header('나만의 식량 바구니') + with cols[-1]: + login_button(login=login, user=user) + button_css() def basket_feedback(): st.markdown("
방금 추천받은 장바구니 어땠나요?
", unsafe_allow_html=True) @@ -71,3 +31,42 @@ def basket_feedback(): st.button('좋아요') with cols[2]: st.button('싫어요') + +def get_response(formatted_url): + response = requests.get(formatted_url) + if response.status_code == 200: + data = response.json() + else: + print(f'status code: {response.status_code}') + data = None + return data + +def patch_feedback(user_id, recipe_id, current_state): + url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/{recipe_id}/feedback" + data = { + 'feedback': not current_state + } + response = requests.patch(url.format(user_id=user_id, recipe_id=recipe_id), json=data) + print(f'status code: {response.status_code}') + st.rerun() + +def button_css(): + st.markdown( + """""", + unsafe_allow_html=True, + ) + +# button[kind="primary"] { +# } +# button[kind="seondary"] { +# div.stButton button { +# width: 150px; +# border: none !important; +# } From f50125fc82f98459e132198f34fee50dd146e075 Mon Sep 17 00:00:00 2001 From: GangBean Date: Tue, 12 Mar 2024 17:18:19 +0900 Subject: [PATCH 039/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20API=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11 --- .../api/routes/users/controller/__init__.py | 0 .../users/controller/request/__init__.py | 0 .../controller/request/signup_request.py | 78 +++++++++++++++++++ .../controller/response/signup_response.py | 8 ++ .../users/controller/user_controller.py | 55 +++++++++++++ backend/app/api/routes/users/dto/user_dto.py | 14 ++++ .../app/api/routes/users/entity/__init__.py | 0 backend/app/api/routes/users/entity/user.py | 7 ++ .../api/routes/users/repository/__init__.py | 0 .../users/repository/user_repository.py | 16 ++++ .../app/api/routes/users/service/__init__.py | 0 .../api/routes/users/service/user_service.py | 15 ++++ 12 files changed, 193 insertions(+) create mode 100644 backend/app/api/routes/users/controller/__init__.py create mode 100644 backend/app/api/routes/users/controller/request/__init__.py create mode 100644 backend/app/api/routes/users/controller/request/signup_request.py create mode 100644 backend/app/api/routes/users/controller/response/signup_response.py create mode 100644 backend/app/api/routes/users/controller/user_controller.py create mode 100644 backend/app/api/routes/users/dto/user_dto.py create mode 100644 backend/app/api/routes/users/entity/__init__.py create mode 100644 backend/app/api/routes/users/entity/user.py create mode 100644 backend/app/api/routes/users/repository/__init__.py create mode 100644 backend/app/api/routes/users/repository/user_repository.py create mode 100644 backend/app/api/routes/users/service/__init__.py create mode 100644 backend/app/api/routes/users/service/user_service.py diff --git a/backend/app/api/routes/users/controller/__init__.py b/backend/app/api/routes/users/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/controller/request/__init__.py b/backend/app/api/routes/users/controller/request/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/controller/request/signup_request.py b/backend/app/api/routes/users/controller/request/signup_request.py new file mode 100644 index 0000000..a2d4ff0 --- /dev/null +++ b/backend/app/api/routes/users/controller/request/signup_request.py @@ -0,0 +1,78 @@ +import re + +from pydantic import BaseModel, validator +from ......exception.users.signup_exeption import ( + UserSignUpInvalidLoginIdException, UserSignUpLoginIdMissningException, + UserSignUpPasswordMissningException, UserSignUpInvalidPasswordException, + UserSignUpNicknameMissningException, UserSignUpInvalidNicknameException, + UserSignUpEmailMissningException, UserSignUpInvalidEmailException, +) + +MINIMUM_LOGIN_ID_LENGTH = 5 +MINIMUM_PASSWORD_LENGTH = 8 +MINIMUM_FAVOR_RECIPE_COUNT = 10 + +class SignupRequest(BaseModel): + login_id: str + password: str + nickname: str + email: str + + @validator('login_id') + def validate_login_id(cls, login_id: str): + if not login_id.strip(): + raise UserSignUpLoginIdMissningException(f"로그인 ID는 필수 입력입니다.") + if len(login_id) < MINIMUM_LOGIN_ID_LENGTH: + raise UserSignUpInvalidLoginIdException(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + + @validator('password') + def validate_password(cls, password: str): + if not password.strip(): + raise UserSignUpPasswordMissningException(f"비밀번호는 필수 입력입니다.") + if len(password) < MINIMUM_PASSWORD_LENGTH: + raise UserSignUpInvalidPasswordException(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + + @validator('nickname') + def validate_nickname(cls, nickname: str): + if not nickname.strip(): + raise UserSignUpNicknameMissningException(f"닉네임은 필수 입력입니다.") + + @validator('email') + def validate_email(cls, email: str): + if not email.strip(): + raise UserSignUpEmailMissningException(f"이메일은 필수 입력입니다.") + if not cls._valid_email(email): + raise UserSignUpInvalidEmailException(f"이메일 형식에 맞지 않습니다: {email}") + + @staticmethod + def _valid_email(email: str) -> bool: + pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$' + return re.match(pattern, email) + + +class LoginRequest(BaseModel): + login_id: str + password: str + + @validator('login_id') + def validate_login_id(cls, login_id: str): + if not login_id.strip(): + raise UserSignUpLoginIdMissningException(f"로그인 ID는 필수 입력입니다.") + if len(login_id) < MINIMUM_LOGIN_ID_LENGTH: + raise UserSignUpInvalidLoginIdException(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + + @validator('password') + def validate_password(cls, password: str): + if not password.strip(): + raise UserSignUpPasswordMissningException(f"비밀번호는 필수 입력입니다.") + if len(password) < MINIMUM_PASSWORD_LENGTH: + raise UserSignUpInvalidPasswordException(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + + +class UserFavorRecipesRequest(BaseModel): + recipes: list[str] + + @validator('recipes') + def validate_login_id(cls, recipes: list[str]): + if len(recipes) < MINIMUM_FAVOR_RECIPE_COUNT: + raise UserSignUpInvalidLoginIdException(f"좋아하는 레시피는 최소 {MINIMUM_FAVOR_RECIPE_COUNT} 개 이상이어야 합니다: {len(recipes)}") diff --git a/backend/app/api/routes/users/controller/response/signup_response.py b/backend/app/api/routes/users/controller/response/signup_response.py new file mode 100644 index 0000000..52510b0 --- /dev/null +++ b/backend/app/api/routes/users/controller/response/signup_response.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + +class SignupResponse(BaseModel): + _id: str + login_id: str + password: str + nickname: str + email: str \ No newline at end of file diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py new file mode 100644 index 0000000..8febef9 --- /dev/null +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -0,0 +1,55 @@ +from fastapi import APIRouter, Request, Response, status + +from .request.signup_request import SignupRequest, LoginRequest +from .response.signup_response import SignupResponse +from ..service.user_service import UserService +from ..dto.user_dto import UserSignupDTO + + +class UserController: + def __init__(self, user_service: UserService): + self.service: UserService = user_service + + def sign_up(self, signup_request: SignupRequest) -> SignupResponse: + return SignupResponse( + **self.service.signup( + UserSignupDTO(**signup_request) + ) + ) + +user_controller = UserController(UserService()) +router = APIRouter('/api/users') + +@router.post('/') +async def sign_up(request: SignupRequest) -> Response: + # response_body = await user_controller.sign_up(UserSignupDTO( + # login_id=request.login_id, + # password=request.password, + # nickname=request.nickname, + # email=request.email)) + # return Response(content=SignupResponse(**response_body), status_code=status.HTTP_200_OK) + pass + +@router.post('/auth') +async def login(request: LoginRequest) -> Response: + pass + +# GET /api/users?login_id={login_id} +@router.get('/') +async def is_usable_login_id(login_id: str) -> Response: + pass + +# GET /api/users?nickname={nickname} +@router.get('/') +async def is_usable_nickname(nickname: str) -> Response: + pass + +# GET /api/foods?page={page_num} +@router.get('/') +async def favor_recipes(page_num: int=1) -> Response: + pass + +# POST /api/users/{user_id}/foods +@router.post('/{user_id}/foods') +async def save_favor_recipes(user_id: str, UserFavorRequest) -> Response: + pass \ No newline at end of file diff --git a/backend/app/api/routes/users/dto/user_dto.py b/backend/app/api/routes/users/dto/user_dto.py new file mode 100644 index 0000000..f92ab89 --- /dev/null +++ b/backend/app/api/routes/users/dto/user_dto.py @@ -0,0 +1,14 @@ +from typing import Optional +from pydantic import BaseModel + +class UserSignupDTO(BaseModel): + _id: Optional[str] + login_id: str + password: str + nickname: str + email: str + +class UserLoginDTO(BaseModel): + _id: Optional[str] + login_id: str + password: str \ No newline at end of file diff --git a/backend/app/api/routes/users/entity/__init__.py b/backend/app/api/routes/users/entity/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/entity/user.py b/backend/app/api/routes/users/entity/user.py new file mode 100644 index 0000000..7090c7c --- /dev/null +++ b/backend/app/api/routes/users/entity/user.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + +class User(BaseModel): + login_id: str + password: str + nickname: str + email: str \ No newline at end of file diff --git a/backend/app/api/routes/users/repository/__init__.py b/backend/app/api/routes/users/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py new file mode 100644 index 0000000..5cd7e32 --- /dev/null +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -0,0 +1,16 @@ +from .....database.data_source import data_source +from ..dto.user_dto import UserSignupDTO + +class UserRepository: + def __init__(self): + self.collection = data_source.collection_with_name_as('users') + + def insert_one(self, user: UserSignupDTO) -> UserSignupDTO: + self.collection.insert_one(user) + return UserSignupDTO(**self.collection.find_one(user)) + + def all_users(self) -> list[UserSignupDTO]: + return [UserSignupDTO(**user) for user in self.collection.find()] + + def find_one(self, id: str) -> UserSignupDTO: + return UserSignupDTO(**self.collection.find_one({'_id': id})) \ No newline at end of file diff --git a/backend/app/api/routes/users/service/__init__.py b/backend/app/api/routes/users/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py new file mode 100644 index 0000000..3e7fbf4 --- /dev/null +++ b/backend/app/api/routes/users/service/user_service.py @@ -0,0 +1,15 @@ +from ..repository.user_repository import UserRepository +from ..dto.user_dto import ( + UserSignupDTO, UserLoginDTO, +) + +class UserService: + def __init__(self, user_repository): + self.repository: UserRepository = user_repository + + # def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: + # return UserSignupDTO(**{ + # '_id': '', + # 'login_id': '', + # 'password': '', + # }) \ No newline at end of file From f2f61b8146f87d76b2125538b7e540c336c0366d Mon Sep 17 00:00:00 2001 From: twndus Date: Tue, 12 Mar 2024 17:42:21 +0900 Subject: [PATCH 040/187] =?UTF-8?q?refactor:=20=EC=9E=AC=EB=A3=8C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A0=88=EC=8B=9C=ED=94=BC=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(next=5Fpage=5Furl=20=ED=99=9C=EC=9A=A9)?= =?UTF-8?q?=20&=20=EA=B3=B5=ED=86=B5=20=EC=BD=94=EB=93=9C=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D=20=EB=AA=A8=EB=93=88=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6 #7 --- frontend/Home.py | 14 +++- frontend/pages/RecommendationHistoryPage.py | 92 ++++++++++++++------- frontend/pages/RecommendationPage.py | 2 +- frontend/utils.py | 16 ++-- 4 files changed, 82 insertions(+), 42 deletions(-) diff --git a/frontend/Home.py b/frontend/Home.py index a2367e5..d03da30 100644 --- a/frontend/Home.py +++ b/frontend/Home.py @@ -5,12 +5,22 @@ # define session-page def home(): - page_header(False, None) + page_header() # 세션 초기화 if 'page_info' not in st.session_state: st.session_state['page_info'] = 'home' + +# 로그인되어 있다고 가정 +def init(): + st.session_state.user = 1 + st.session_state.is_authenticated = True + +# 로그인 상태 초기화 +init() + + +# page_info 설정 st.session_state['page_info'] = 'home' -# 이전 추천 목록 조회 home() diff --git a/frontend/pages/RecommendationHistoryPage.py b/frontend/pages/RecommendationHistoryPage.py index 61dc0da..51d8b6d 100644 --- a/frontend/pages/RecommendationHistoryPage.py +++ b/frontend/pages/RecommendationHistoryPage.py @@ -1,25 +1,75 @@ +import time, math + import streamlit as st import pandas as pd import numpy as np -import time import requests from utils import page_header, get_response, patch_feedback -user = '주디' -user_id = '1' -page_num = '1' +def show_feedback_button(recipe_id, user_feedback): + + icon_mapper = lambda cooked: '❤️' if cooked else '🩶' + cooked = recipe_id in user_feedback + + st.button( + icon_mapper(cooked), + on_click=patch_feedback, + key=f'{recipe_id}_feedback_button', + args=(st.session_state.user, recipe_id, cooked)) + +def display_recipes_in_rows_of_four(recipe_list, user_feedback=None): + + for row in range(math.ceil(len(recipe_list)/4)): + cols = st.columns(4) + + for i in range(4): + item_idx = i + row * 4 + if item_idx >= len(recipe_list): break + + item = recipe_list[item_idx] + with cols[i]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + if user_feedback is None: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + else: + sub_cols = st.columns([3,1]) + with sub_cols[0]: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + with sub_cols[-1]: + show_feedback_button(item['recipe_id'], user_feedback) + + +def get_and_stack_recipe_data(): + + url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/recommended?page={page_num}" + recipe_list, user_feedback = [], [] + formatted_url = url.format(user_id=st.session_state.user, page_num=1) + + while formatted_url: + print(formatted_url) + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + print(data['user_feedback']) + user_feedback = data['user_feedback'] + + return recipe_list, user_feedback def recommendation_history_page(): # 앱 헤더 - page_header(False, None) + page_header() - url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/recommended?page={page_num}" - user_id, page_num = 1,1 - formatted_url = url.format(user_id=user_id, page_num=page_num) - data = get_response(formatted_url) + # get data + recipe_list, user_feedback = get_and_stack_recipe_data() + +# url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/recommended?page={page_num}" +# user_id, page_num = 1,1 +# formatted_url = url.format(user_id=user_id, page_num=page_num) +# data = get_response(formatted_url) # 페이지 구성 container = st.container(border=True) @@ -33,29 +83,7 @@ def recommendation_history_page(): st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) - recipe_list = data['recipe_list'] - for row in range(int(len(recipe_list)/4)): - cols = st.columns(4) - - for i in range(4): - if row == len(recipe_list)//4: i = len(recipe_list)%4 - - with cols[i]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - sub_cols = st.columns([3,1]) - with sub_cols[0]: - st.markdown(f'

{recipe_list[i]["recipe_name"]}

', unsafe_allow_html=True) - - icon_mapper = lambda cooked: '❤️' if cooked else '🩶' - cooked = recipe_list[i]['recipe_id'] in data['user_feedback'] - - with sub_cols[-1]: - st.button( - icon_mapper(cooked), - on_click=patch_feedback, - key=f'feedback_{i}', - args=(user_id, recipe_list[i]['recipe_id'], cooked)) + display_ingredients_in_rows_of_four(recipe_list, user_feedback) st.session_state['page_info'] = 'recommend_history' recommendation_history_page() diff --git a/frontend/pages/RecommendationPage.py b/frontend/pages/RecommendationPage.py index ee3e4b8..ee28979 100644 --- a/frontend/pages/RecommendationPage.py +++ b/frontend/pages/RecommendationPage.py @@ -9,7 +9,7 @@ def recommendation_page(): # 앱 헤더 - page_header(False, None) + page_header() # 페이지 구성 container = st.container(border=True) diff --git a/frontend/utils.py b/frontend/utils.py index d0fd51d..ff3d9c0 100644 --- a/frontend/utils.py +++ b/frontend/utils.py @@ -2,25 +2,27 @@ import requests def set_logout(): - st.session_state['key'] = None + st.session_state.user = None + st.session_state.is_authenticated = False def set_login(): - st.session_state['key'] = 'session-key' + st.session_state.user = 'judy123' + st.session_state.is_authenticated = True -def login_button(login, user): - if login: - login_button = st.button(f"{user}님 | 로그아웃", on_click=set_logout) +def login_button(): + if st.session_state.is_authenticated: + login_button = st.button(f"{st.session_state.user}님 | 로그아웃", on_click=set_logout) else: login_button = st.button(f"회원가입 | 로그인", on_click=set_login) return login_button -def page_header(login, user): +def page_header(): cols = st.columns([8,3]) with cols[0]: st.header('나만의 식량 바구니') with cols[-1]: - login_button(login=login, user=user) + login_button() button_css() def basket_feedback(): From 00df9d52473b785cb67530afac128c613357083a Mon Sep 17 00:00:00 2001 From: twndus Date: Tue, 12 Mar 2024 17:43:13 +0900 Subject: [PATCH 041/187] =?UTF-8?q?feat(UserHistoryPage,-Result-page-2):?= =?UTF-8?q?=20UserHistoryPage,=20Result-page-2=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8 #9 --- frontend/pages/ResultPage2.py | 69 +++++++++++++++++++++++++++++++ frontend/pages/UserHistoryPage.py | 43 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 frontend/pages/ResultPage2.py create mode 100644 frontend/pages/UserHistoryPage.py diff --git a/frontend/pages/ResultPage2.py b/frontend/pages/ResultPage2.py new file mode 100644 index 0000000..722a256 --- /dev/null +++ b/frontend/pages/ResultPage2.py @@ -0,0 +1,69 @@ +import time, math + +import pandas as pd +import numpy as np + +from PIL import Image +import streamlit as st +import requests + +from utils import page_header, basket_feedback, get_response, patch_feedback +from pages.RecommendationHistoryPage import display_ingredients_in_rows_of_four + +def display_ingredients_in_rows_of_four(ingredients): + for ingredient in ingredients: + sub_container = st.container(border=True) + + with sub_container: + + cols = st.columns(5) + + with cols[0]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + with cols[1]: + st.write(ingredient['ingredient_name']) + st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) + + with cols[-1]: + st.link_button('구매', ingredient['market_url'], type='primary') + +def result_page_2(): + + # 앱 헤더 + page_header() + + url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/previousrecommendation" + formatted_url = url.format(user_id=st.session_state.user) + data = get_response(formatted_url) + + # 페이지 구성 + container = st.container(border=True) + + with container: + + # 장바구니 추천 문구 + st.markdown("

새로운 장바구니를 추천받았어요!

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + + st.divider() + + # 구매할 식료품 목록 + st.markdown("

추천 장바구니

", unsafe_allow_html=True) + + display_ingredients_in_rows_of_four(data['ingredient_list']) + total_price = sum([ingredient['ingredient_price'] for ingredient in data['ingredient_list']]) + + st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) + + st.divider() + + # 이 장바구니로 만들 수 있는 음식 레시피 + st.markdown("

이 장바구니로 만들 수 있는 음식 레시피

", unsafe_allow_html=True) + display_ingredients_in_rows_of_four(data['recipe_list']) + + st.text("\n\n") + basket_feedback() + +st.session_state['page_info'] = "result_page_2" +result_page_2() diff --git a/frontend/pages/UserHistoryPage.py b/frontend/pages/UserHistoryPage.py new file mode 100644 index 0000000..638ebf1 --- /dev/null +++ b/frontend/pages/UserHistoryPage.py @@ -0,0 +1,43 @@ +import time, math + +import pandas as pd +import numpy as np + +import streamlit as st +import requests + +from utils import page_header, get_response, patch_feedback +from pages.RecommendationHistoryPage import display_ingredients_in_rows_of_four + +def get_and_stack_recipe_data(): + + url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/cooked?page={page_num}" + recipe_list = [] + formatted_url = url.format(user_id=st.session_state.user, page_num=1) + + while formatted_url: + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + + return recipe_list + +def user_history_page(): + + # 앱 헤더 + page_header() + + # get data + recipe_list = get_and_stack_recipe_data() + + # show container + container = st.container(border=True) + + with container: + # title + st.markdown("

❤️ 내가 요리한 레시피 ❤️

", unsafe_allow_html=True) + display_ingredients_in_rows_of_four(recipe_list) + +# show UserHistoryPage +st.session_state['page_info'] = 'user_history' +user_history_page() From a0be9ba9bd4aebe8f83ff79bddc6903d0adad1f8 Mon Sep 17 00:00:00 2001 From: twndus Date: Tue, 12 Mar 2024 19:01:55 +0900 Subject: [PATCH 042/187] =?UTF-8?q?chore:=20crawling=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=EB=A1=9C=20=EC=9D=B4=EB=8F=99=20&=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EA=B2=B0=EA=B3=BC=20=EB=B3=91=ED=95=A9=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20#2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawl_price.py => crawling/crawl_price.py | 0 .../crawl_ranked_recipes.py | 0 crawl_recipe.py => crawling/crawl_recipe.py | 0 crawl_review.py => crawling/crawl_review.py | 0 .../crawl_reviewer.py | 0 .../crawl_reviewer_split.py | 0 .../crawl_userid_split.py | 0 crawling/merge_results.py | 29 +++++++++++++++++++ 8 files changed, 29 insertions(+) rename crawl_price.py => crawling/crawl_price.py (100%) rename crawl_ranked_recipes.py => crawling/crawl_ranked_recipes.py (100%) rename crawl_recipe.py => crawling/crawl_recipe.py (100%) rename crawl_review.py => crawling/crawl_review.py (100%) rename crawl_reviewer.py => crawling/crawl_reviewer.py (100%) rename crawl_reviewer_split.py => crawling/crawl_reviewer_split.py (100%) rename crawl_userid_split.py => crawling/crawl_userid_split.py (100%) create mode 100644 crawling/merge_results.py diff --git a/crawl_price.py b/crawling/crawl_price.py similarity index 100% rename from crawl_price.py rename to crawling/crawl_price.py diff --git a/crawl_ranked_recipes.py b/crawling/crawl_ranked_recipes.py similarity index 100% rename from crawl_ranked_recipes.py rename to crawling/crawl_ranked_recipes.py diff --git a/crawl_recipe.py b/crawling/crawl_recipe.py similarity index 100% rename from crawl_recipe.py rename to crawling/crawl_recipe.py diff --git a/crawl_review.py b/crawling/crawl_review.py similarity index 100% rename from crawl_review.py rename to crawling/crawl_review.py diff --git a/crawl_reviewer.py b/crawling/crawl_reviewer.py similarity index 100% rename from crawl_reviewer.py rename to crawling/crawl_reviewer.py diff --git a/crawl_reviewer_split.py b/crawling/crawl_reviewer_split.py similarity index 100% rename from crawl_reviewer_split.py rename to crawling/crawl_reviewer_split.py diff --git a/crawl_userid_split.py b/crawling/crawl_userid_split.py similarity index 100% rename from crawl_userid_split.py rename to crawling/crawl_userid_split.py diff --git a/crawling/merge_results.py b/crawling/merge_results.py new file mode 100644 index 0000000..223287f --- /dev/null +++ b/crawling/merge_results.py @@ -0,0 +1,29 @@ +import os +from datetime import datetime as dt + +import numpy as np +import pandas as pd + +def main(): + today = dt.now().strftime('%y%m%d') + all_files = os.listdir('usercrawlresult') + + recipes, reviews = [], [] + + for filename in all_files: + if filename.startswith('recipes'): + recipes.append(pd.read_csv('usercrawlresult/'+filename)) + elif filename.startswith('reviews'): + reviews.append(pd.read_csv('usercrawlresult/'+filename)) + + print(f'num of recipes files: {len(recipes)}') + print(f'num of reviews files: {len(reviews)}') + + recipes = pd.concat(recipes, axis=0) + reviews = pd.concat(reviews, axis=0) + + recipes.to_csv(f'usercrawlresult/recipes_full_{today}.csv', index=False) + reviews.to_csv(f'usercrawlresult/reviews_full_{today}.csv', index=False) + +if __name__ == '__main__': + main() From 04d0e1f66015dbc731e79f58b0204d407c6e8ae6 Mon Sep 17 00:00:00 2001 From: GangBean Date: Tue, 12 Mar 2024 20:05:43 +0900 Subject: [PATCH 043/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20API=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11 --- README.xxx | 4 ++ .../users/controller/user_controller.py | 56 +++++++++++++------ 2 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 README.xxx diff --git a/README.xxx b/README.xxx new file mode 100644 index 0000000..e282165 --- /dev/null +++ b/README.xxx @@ -0,0 +1,4 @@ +./configure +make +make test +sudo make install diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 8febef9..5c06b40 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -1,9 +1,9 @@ from fastapi import APIRouter, Request, Response, status -from .request.signup_request import SignupRequest, LoginRequest -from .response.signup_response import SignupResponse +from .request.signup_request import SignupRequest, LoginRequest, UserFavorRecipesRequest +from .response.signup_response import SignupResponse, LoginResponse, FavorRecipesResponse from ..service.user_service import UserService -from ..dto.user_dto import UserSignupDTO +from ..dto.user_dto import UserSignupDTO, UserLoginDTO class UserController: @@ -16,40 +16,60 @@ def sign_up(self, signup_request: SignupRequest) -> SignupResponse: UserSignupDTO(**signup_request) ) ) + + def is_login_id_usable(self, login_id: str) -> bool: + return self.service.is_login_id_usable(login_id) + + def is_nickname_usable(self, nickname: str) -> bool: + return self.service.is_nickname_usable(nickname) + + def favor_recipes(self, page_num: int) -> list: + return self.service.favor_recipes(page_num) + + def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: + return self.service.save_favor_recipes(login_id, request) + user_controller = UserController(UserService()) router = APIRouter('/api/users') -@router.post('/') +@router.post() async def sign_up(request: SignupRequest) -> Response: - # response_body = await user_controller.sign_up(UserSignupDTO( - # login_id=request.login_id, - # password=request.password, - # nickname=request.nickname, - # email=request.email)) - # return Response(content=SignupResponse(**response_body), status_code=status.HTTP_200_OK) - pass + response_body = await user_controller.sign_up(UserSignupDTO( + login_id=request.login_id, + password=request.password, + nickname=request.nickname, + email=request.email)) + return Response(content=SignupResponse(**response_body), status_code=status.HTTP_200_OK) @router.post('/auth') async def login(request: LoginRequest) -> Response: - pass + response_body = await user_controller.login(UserLoginDTO( + login_id=request.login_id, + password=request.password + )) + return Response(content=LoginResponse(**response_body), status_code=status.HTTP_200_OK) # GET /api/users?login_id={login_id} -@router.get('/') +@router.get() async def is_usable_login_id(login_id: str) -> Response: - pass + await user_controller.is_login_id_usable(login_id) + return Response(status_code=status.HTTP_200_OK) # GET /api/users?nickname={nickname} @router.get('/') async def is_usable_nickname(nickname: str) -> Response: - pass + await user_controller.is_nickname_usable(nickname) + return Response(status_code=status.HTTP_200_OK) # GET /api/foods?page={page_num} @router.get('/') async def favor_recipes(page_num: int=1) -> Response: - pass + response_body = await user_controller.favor_recipes(page_num) + return Response(content=FavorRecipesResponse(**response_body), status_code=status.HTTP_200_OK) # POST /api/users/{user_id}/foods @router.post('/{user_id}/foods') -async def save_favor_recipes(user_id: str, UserFavorRequest) -> Response: - pass \ No newline at end of file +async def save_favor_recipes(user_id: str, request: UserFavorRecipesRequest) -> Response: + await user_controller.save_favor_recipes(user_id, request) + return Response(status_code=status.HTTP_200_OK) From 81e4ec36edf3787aee3e6730d50d2805444e48d3 Mon Sep 17 00:00:00 2001 From: GangBean Date: Tue, 12 Mar 2024 20:09:41 +0900 Subject: [PATCH 044/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20service=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=EC=A1=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11 --- .../api/routes/users/service/user_service.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index 3e7fbf4..a28c673 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -2,14 +2,23 @@ from ..dto.user_dto import ( UserSignupDTO, UserLoginDTO, ) +from ..controller.request.signup_request import UserFavorRecipesRequest class UserService: def __init__(self, user_repository): self.repository: UserRepository = user_repository - # def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: - # return UserSignupDTO(**{ - # '_id': '', - # 'login_id': '', - # 'password': '', - # }) \ No newline at end of file + def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: + return None + + def is_login_id_usable(self, login_id: str) -> bool: + return False + + def is_nickname_usable(self, nickname: str) -> bool: + return False + + def favor_recipes(self, page_num: int) -> list: + return list() + + def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: + return None From 8bdff7d96083664743933d7f3f560020df120f26 Mon Sep 17 00:00:00 2001 From: GangBean Date: Wed, 13 Mar 2024 08:38:05 +0900 Subject: [PATCH 045/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=EC=B2=B4=ED=81=AC,=20=EC=84=A0=ED=98=B8?= =?UTF-8?q?=EC=83=81=ED=92=88=EC=A1=B0=ED=9A=8C,=20=EC=84=A0=ED=98=B8?= =?UTF-8?q?=EC=83=81=ED=92=88=EB=93=B1=EB=A1=9D=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11 --- backend/app/api/routes/users/__init__.py | 0 .../controller/request/signup_request.py | 37 ++++----- .../controller/response/signup_response.py | 8 +- .../users/controller/user_controller.py | 79 ++++++++++++------- backend/app/api/routes/users/dto/user_dto.py | 2 +- backend/app/api/routes/users/entity/user.py | 8 +- .../users/repository/user_repository.py | 56 +++++++++++-- .../api/routes/users/service/user_service.py | 35 ++++++-- 8 files changed, 161 insertions(+), 64 deletions(-) create mode 100644 backend/app/api/routes/users/__init__.py diff --git a/backend/app/api/routes/users/__init__.py b/backend/app/api/routes/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/controller/request/signup_request.py b/backend/app/api/routes/users/controller/request/signup_request.py index a2d4ff0..bf3591d 100644 --- a/backend/app/api/routes/users/controller/request/signup_request.py +++ b/backend/app/api/routes/users/controller/request/signup_request.py @@ -1,12 +1,6 @@ import re from pydantic import BaseModel, validator -from ......exception.users.signup_exeption import ( - UserSignUpInvalidLoginIdException, UserSignUpLoginIdMissningException, - UserSignUpPasswordMissningException, UserSignUpInvalidPasswordException, - UserSignUpNicknameMissningException, UserSignUpInvalidNicknameException, - UserSignUpEmailMissningException, UserSignUpInvalidEmailException, -) MINIMUM_LOGIN_ID_LENGTH = 5 MINIMUM_PASSWORD_LENGTH = 8 @@ -21,28 +15,32 @@ class SignupRequest(BaseModel): @validator('login_id') def validate_login_id(cls, login_id: str): if not login_id.strip(): - raise UserSignUpLoginIdMissningException(f"로그인 ID는 필수 입력입니다.") + raise ValueError(f"로그인 ID는 필수 입력입니다.") if len(login_id) < MINIMUM_LOGIN_ID_LENGTH: - raise UserSignUpInvalidLoginIdException(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + raise ValueError(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + return login_id @validator('password') def validate_password(cls, password: str): if not password.strip(): - raise UserSignUpPasswordMissningException(f"비밀번호는 필수 입력입니다.") + raise ValueError(f"비밀번호는 필수 입력입니다.") if len(password) < MINIMUM_PASSWORD_LENGTH: - raise UserSignUpInvalidPasswordException(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + raise ValueError(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + return password @validator('nickname') def validate_nickname(cls, nickname: str): if not nickname.strip(): - raise UserSignUpNicknameMissningException(f"닉네임은 필수 입력입니다.") + raise ValueError(f"닉네임은 필수 입력입니다.") + return nickname @validator('email') def validate_email(cls, email: str): if not email.strip(): - raise UserSignUpEmailMissningException(f"이메일은 필수 입력입니다.") + raise ValueError(f"이메일은 필수 입력입니다.") if not cls._valid_email(email): - raise UserSignUpInvalidEmailException(f"이메일 형식에 맞지 않습니다: {email}") + raise ValueError(f"이메일 형식에 맞지 않습니다: {email}") + return email @staticmethod def _valid_email(email: str) -> bool: @@ -57,16 +55,18 @@ class LoginRequest(BaseModel): @validator('login_id') def validate_login_id(cls, login_id: str): if not login_id.strip(): - raise UserSignUpLoginIdMissningException(f"로그인 ID는 필수 입력입니다.") + raise ValueError(f"로그인 ID는 필수 입력입니다.") if len(login_id) < MINIMUM_LOGIN_ID_LENGTH: - raise UserSignUpInvalidLoginIdException(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + raise ValueError(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + return login_id @validator('password') def validate_password(cls, password: str): if not password.strip(): - raise UserSignUpPasswordMissningException(f"비밀번호는 필수 입력입니다.") + raise ValueError(f"비밀번호는 필수 입력입니다.") if len(password) < MINIMUM_PASSWORD_LENGTH: - raise UserSignUpInvalidPasswordException(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + raise ValueError(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + return password class UserFavorRecipesRequest(BaseModel): @@ -75,4 +75,5 @@ class UserFavorRecipesRequest(BaseModel): @validator('recipes') def validate_login_id(cls, recipes: list[str]): if len(recipes) < MINIMUM_FAVOR_RECIPE_COUNT: - raise UserSignUpInvalidLoginIdException(f"좋아하는 레시피는 최소 {MINIMUM_FAVOR_RECIPE_COUNT} 개 이상이어야 합니다: {len(recipes)}") + raise ValueError(f"좋아하는 레시피는 최소 {MINIMUM_FAVOR_RECIPE_COUNT} 개 이상이어야 합니다: {len(recipes)}") + return recipes diff --git a/backend/app/api/routes/users/controller/response/signup_response.py b/backend/app/api/routes/users/controller/response/signup_response.py index 52510b0..3b769bd 100644 --- a/backend/app/api/routes/users/controller/response/signup_response.py +++ b/backend/app/api/routes/users/controller/response/signup_response.py @@ -5,4 +5,10 @@ class SignupResponse(BaseModel): login_id: str password: str nickname: str - email: str \ No newline at end of file + email: str + +class LoginResponse(BaseModel): + _id: str + +class FavorRecipesResponse(BaseModel): + recipes: list \ No newline at end of file diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 5c06b40..33b6cab 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -1,75 +1,96 @@ -from fastapi import APIRouter, Request, Response, status +from fastapi import APIRouter, Query, Request, Response, status +from typing import Optional + +import logging +from fastapi.responses import JSONResponse + +from pydantic import BaseModel from .request.signup_request import SignupRequest, LoginRequest, UserFavorRecipesRequest from .response.signup_response import SignupResponse, LoginResponse, FavorRecipesResponse from ..service.user_service import UserService +from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository from ..dto.user_dto import UserSignupDTO, UserLoginDTO - class UserController: def __init__(self, user_service: UserService): self.service: UserService = user_service - def sign_up(self, signup_request: SignupRequest) -> SignupResponse: + async def sign_up(self, signup_request: SignupRequest) -> SignupResponse: return SignupResponse( - **self.service.signup( - UserSignupDTO(**signup_request) - ) + **dict(self.service.sign_up( + UserSignupDTO(**dict(signup_request)) + )) + ) + + async def login(self, login_request: UserLoginDTO) -> LoginResponse: + return LoginResponse( + **dict(self.service.login(login_request)) ) - def is_login_id_usable(self, login_id: str) -> bool: + async def is_login_id_usable(self, login_id: str) -> bool: return self.service.is_login_id_usable(login_id) - def is_nickname_usable(self, nickname: str) -> bool: + async def is_nickname_usable(self, nickname: str) -> bool: return self.service.is_nickname_usable(nickname) - def favor_recipes(self, page_num: int) -> list: + async def favor_recipes(self, page_num: int) -> list: return self.service.favor_recipes(page_num) - def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: + async def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: return self.service.save_favor_recipes(login_id, request) -user_controller = UserController(UserService()) -router = APIRouter('/api/users') +user_controller = UserController(UserService( + UserRepository(), SessionRepository(), FoodRepository())) +user_router = APIRouter() -@router.post() -async def sign_up(request: SignupRequest) -> Response: +class Request(BaseModel): + login_id: str + password: str + nickname: str + email: str + +@user_router.post('/api/users') +async def sign_up(request: SignupRequest) -> JSONResponse: + # logging.info(request) response_body = await user_controller.sign_up(UserSignupDTO( login_id=request.login_id, password=request.password, nickname=request.nickname, email=request.email)) - return Response(content=SignupResponse(**response_body), status_code=status.HTTP_200_OK) + return JSONResponse(content=response_body.model_dump(), status_code=status.HTTP_200_OK) -@router.post('/auth') +@user_router.post('/api/users/auth') async def login(request: LoginRequest) -> Response: + # logging.info(request) response_body = await user_controller.login(UserLoginDTO( login_id=request.login_id, password=request.password )) - return Response(content=LoginResponse(**response_body), status_code=status.HTTP_200_OK) + logging.debug(response_body) + return JSONResponse(content=response_body.model_dump(), status_code=status.HTTP_200_OK) # GET /api/users?login_id={login_id} -@router.get() -async def is_usable_login_id(login_id: str) -> Response: - await user_controller.is_login_id_usable(login_id) - return Response(status_code=status.HTTP_200_OK) - -# GET /api/users?nickname={nickname} -@router.get('/') -async def is_usable_nickname(nickname: str) -> Response: - await user_controller.is_nickname_usable(nickname) +@user_router.get('/api/users') +async def validate_duplicate_info( + login_id: Optional[str]=Query(None), + nickname: Optional[str]=Query(None), +) -> Response: + if login_id and login_id.strip(): + await user_controller.is_login_id_usable(login_id) + if nickname and nickname.strip(): + await user_controller.is_nickname_usable(nickname) return Response(status_code=status.HTTP_200_OK) # GET /api/foods?page={page_num} -@router.get('/') +@user_router.get('/api/users/foods') async def favor_recipes(page_num: int=1) -> Response: response_body = await user_controller.favor_recipes(page_num) - return Response(content=FavorRecipesResponse(**response_body), status_code=status.HTTP_200_OK) + return JSONResponse(content=response_body, status_code=status.HTTP_200_OK) # POST /api/users/{user_id}/foods -@router.post('/{user_id}/foods') +@user_router.post('/api/users/{user_id}/foods') async def save_favor_recipes(user_id: str, request: UserFavorRecipesRequest) -> Response: await user_controller.save_favor_recipes(user_id, request) return Response(status_code=status.HTTP_200_OK) diff --git a/backend/app/api/routes/users/dto/user_dto.py b/backend/app/api/routes/users/dto/user_dto.py index f92ab89..546adff 100644 --- a/backend/app/api/routes/users/dto/user_dto.py +++ b/backend/app/api/routes/users/dto/user_dto.py @@ -9,6 +9,6 @@ class UserSignupDTO(BaseModel): email: str class UserLoginDTO(BaseModel): - _id: Optional[str] + token: str | None = None login_id: str password: str \ No newline at end of file diff --git a/backend/app/api/routes/users/entity/user.py b/backend/app/api/routes/users/entity/user.py index 7090c7c..f8c94f7 100644 --- a/backend/app/api/routes/users/entity/user.py +++ b/backend/app/api/routes/users/entity/user.py @@ -1,7 +1,13 @@ from pydantic import BaseModel +from typing import Optional class User(BaseModel): + _id: Optional[str] login_id: str password: str nickname: str - email: str \ No newline at end of file + email: str + +class Session(BaseModel): + _id: Optional[str] + user_id: str \ No newline at end of file diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index 5cd7e32..58d73c9 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -1,16 +1,60 @@ -from .....database.data_source import data_source -from ..dto.user_dto import UserSignupDTO +from datetime import datetime +from database.data_source import data_source +from ..dto.user_dto import UserSignupDTO, UserLoginDTO +import logging +import pymongo + +logging.basicConfig(level=logging.DEBUG) class UserRepository: def __init__(self): self.collection = data_source.collection_with_name_as('users') def insert_one(self, user: UserSignupDTO) -> UserSignupDTO: - self.collection.insert_one(user) - return UserSignupDTO(**self.collection.find_one(user)) + result = self.collection.insert_one(dict(user)) + return UserSignupDTO( + _id=result.inserted_id, + login_id=user.login_id, + password=user.password, + nickname=user.nickname, + email=user.email) def all_users(self) -> list[UserSignupDTO]: return [UserSignupDTO(**user) for user in self.collection.find()] - def find_one(self, id: str) -> UserSignupDTO: - return UserSignupDTO(**self.collection.find_one({'_id': id})) \ No newline at end of file + def find_one(self, query: dict) -> UserSignupDTO: + result = self.collection.find_one(query) + logging.debug(result) + return result + + def update_food(self, login_id: str, foods: list) -> int: + query = {'login_id': login_id} + update_value = {'$set': {'initial_feedback_history': foods}} + result = self.collection.update_one(query, update_value) + return result.modified_count + +class SessionRepository: + def __init__(self): + self.collection = data_source.collection_with_name_as('sessions') + + def insert_one(self, login_id: str, token: str, expire_date: datetime) -> UserLoginDTO: + result = self.collection.insert_one({'login_id': login_id, 'token': token, 'expire_date': expire_date}) + if result.inserted_id is None: + raise ValueError("세션 생성이 실패했습니다.") + return UserLoginDTO(token=token, login_id=login_id, password='') + + def find_one(self, id: str) -> UserLoginDTO: + return UserLoginDTO(**self.collection.find_one({'_id': id})) + +class FoodRepository: + def __init__(self): + self.collection = data_source.collection_with_name_as('foods') + + def find_foods(self, page_num: int, page_size: int=10) -> list: + skip_count: int = (page_num - 1) * page_size + results = self.collection.find().sort([('name', pymongo.ASCENDING)]).skip(skip_count).limit(page_size) + lst = [] + for result in results: + result['_id'] = str(result['_id']) + lst.append(result) + return lst \ No newline at end of file diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index a28c673..ce71e0f 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -1,24 +1,43 @@ -from ..repository.user_repository import UserRepository +import uuid +from datetime import datetime, timedelta + +from ..entity.user import User +from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository from ..dto.user_dto import ( UserSignupDTO, UserLoginDTO, ) from ..controller.request.signup_request import UserFavorRecipesRequest class UserService: - def __init__(self, user_repository): - self.repository: UserRepository = user_repository + def __init__(self, user_repository :UserRepository, session_repository: SessionRepository, food_repository: FoodRepository): + self.user_repository: UserRepository = user_repository + self.session_repository: SessionRepository = session_repository + self.food_repository: FoodRepository = food_repository def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: - return None + return self.user_repository.insert_one(sign_up_request) + + def login(self, login_request: UserLoginDTO) -> UserLoginDTO: + user = User(**dict(self.user_repository.find_one({'login_id': login_request.login_id, 'password': login_request.password}))) + if user is None: + raise ValueError("아이디와 비밀번호가 일치하지 않습니다.") + + token = str(uuid.uuid4()) + expire_date = datetime.utcnow() + timedelta(seconds=30 * 60) + return self.session_repository.insert_one(login_id=login_request.login_id, token=token, expire_date=expire_date) def is_login_id_usable(self, login_id: str) -> bool: - return False + if self.user_repository.find_one({'login_id': login_id}) is not None: + raise ValueError(f"중복되는 아이디 입니다: {login_id}") + return True def is_nickname_usable(self, nickname: str) -> bool: - return False + if self.user_repository.find_one({'nickname': nickname}) is not None: + raise ValueError(f"중복되는 닉네임 입니다: {nickname}") + return True def favor_recipes(self, page_num: int) -> list: - return list() + return self.food_repository.find_foods(page_num) def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: - return None + self.user_repository.update_food(login_id, request.recipes) From 0da66341515668e72478ed5c51d8d8b76bafecad Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Wed, 13 Mar 2024 10:46:44 +0900 Subject: [PATCH 046/187] =?UTF-8?q?feat(controller):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../api/routes/recipes/controller/__init__.py | 0 .../recipes/controller/recipes_controller.py | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 backend/app/api/routes/recipes/controller/__init__.py create mode 100644 backend/app/api/routes/recipes/controller/recipes_controller.py diff --git a/backend/app/api/routes/recipes/controller/__init__.py b/backend/app/api/routes/recipes/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py new file mode 100644 index 0000000..d7c2cd7 --- /dev/null +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter +from ..service.recipes_service import get_user_cooked_recipes, get_recipes_by_recipes_id, get_ingredients_collections_by_recipes +from ..dto.get_recipes_reponse_list import GetRecipesReponseList + +recipes_router = APIRouter() + +@recipes_router.get( + "/users/{user_id}/recipes/cooked", + response_description="유저가 요리한 레시피 목록", + response_model=GetRecipesReponseList + ) +def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): + # user_id로 유저가 요리한 레시피 id 리스트 조회 + user_cooked_recipes_id = get_user_cooked_recipes(user_id) + # 레시피 id 리스트로 각 레시피의 정보 조회 + recipes = get_recipes_by_recipes_id(user_cooked_recipes_id) + # 레시피로 Ingredients 리스트 조회 + ingredients_collection = get_ingredients_collections_by_recipes(recipes) + return GetRecipesReponseList(recipes, ingredients_collection) \ No newline at end of file From 7b8e62ab709a9c4dd732207a459c6641e6db4280 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Wed, 13 Mar 2024 10:49:08 +0900 Subject: [PATCH 047/187] =?UTF-8?q?feat(service):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../api/routes/recipes/service/__init__.py | 0 .../routes/recipes/service/recipes_service.py | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 backend/app/api/routes/recipes/service/__init__.py create mode 100644 backend/app/api/routes/recipes/service/recipes_service.py diff --git a/backend/app/api/routes/recipes/service/__init__.py b/backend/app/api/routes/recipes/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py new file mode 100644 index 0000000..0b9a0df --- /dev/null +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -0,0 +1,24 @@ +from typing import List +from ..repository.recipes_repository import select_user_by_user_id, select_recipes_by_recipes_id, select_ingredients_by_ingredients_id +from ..entity.recipes import Recipes + + +def get_user_cooked_recipes(user_id: str) -> List[str]: + user = select_user_by_user_id(user_id) + user_cooked_recipes = user.get_feedback_history() + return user_cooked_recipes + + +def get_recipes_by_recipes_id(recipes_id: List[str]) -> Recipes: + # 레시피 리스트 조회 + recipes = select_recipes_by_recipes_id(recipes_id) + return recipes + + +def get_ingredients_collections_by_recipes(recipes: Recipes) -> List[Recipes]: + # 레시피 별로 재료 리스트 조회: + # [Ingredients(ingredients: [Ingredient, Ingredient]), Ingredients(ingredients: [Ingredient, Ingredient])] + ingredients_collection = [select_ingredients_by_ingredients_id(recipe.get_ingredients()) for recipe in recipes.get_recipes()] + return ingredients_collection + + \ No newline at end of file From 094fd14dfc15235f181881b779c8bf2a17188838 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Wed, 13 Mar 2024 10:55:21 +0900 Subject: [PATCH 048/187] =?UTF-8?q?feat(entity):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20entity=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../app/api/routes/recipes/entity/__init__.py | 0 .../app/api/routes/recipes/entity/recipe.py | 52 +++++++++++++++++++ .../app/api/routes/recipes/entity/recipes.py | 11 ++++ backend/app/api/routes/recipes/entity/user.py | 43 +++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 backend/app/api/routes/recipes/entity/__init__.py create mode 100644 backend/app/api/routes/recipes/entity/recipe.py create mode 100644 backend/app/api/routes/recipes/entity/recipes.py create mode 100644 backend/app/api/routes/recipes/entity/user.py diff --git a/backend/app/api/routes/recipes/entity/__init__.py b/backend/app/api/routes/recipes/entity/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/recipes/entity/recipe.py b/backend/app/api/routes/recipes/entity/recipe.py new file mode 100644 index 0000000..72e0080 --- /dev/null +++ b/backend/app/api/routes/recipes/entity/recipe.py @@ -0,0 +1,52 @@ +from pydantic import BaseModel, Field, ConfigDict +from .....utils.pyobject_id import PyObjectId +from typing import List + + +class Recipe(BaseModel): + id: PyObjectId = Field(alias='_id', default=None) + food_name: str + recipe_name: str + ingredient: List[PyObjectId] = [] + time_taken: int + difficulty: str + recipe_url: str + portion: str + recipe_img_url: str + model_config = ConfigDict( + populate_by_name=True, + arbitrary_types_allowed=True, + json_schema_extra={ + "example": { + "id": "recipe_id", + "food_name": "김치찌개", + "recipe_name": "매콤한 김치찌개", + "ingredient": [], + "time_taken": 30, + "difficulty": "중급", + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "portion": "4인분", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + } + }, + ) + + + def get_id(self): + return self.id + + + def get_recipe_name(self): + return self.recipe_name + + + def get_recipe_url(self): + return self.recipe_url + + + def get_recipe_img_url(self): + return self.recipe_img_url + + + def get_ingredients(self): + return self.ingredient diff --git a/backend/app/api/routes/recipes/entity/recipes.py b/backend/app/api/routes/recipes/entity/recipes.py new file mode 100644 index 0000000..5909118 --- /dev/null +++ b/backend/app/api/routes/recipes/entity/recipes.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel +from typing import List +from .recipe import Recipe + + +class Recipes(BaseModel): + recipes: List[Recipe] + + def get_recipes(self): + return self.recipes + \ No newline at end of file diff --git a/backend/app/api/routes/recipes/entity/user.py b/backend/app/api/routes/recipes/entity/user.py new file mode 100644 index 0000000..3eb5c63 --- /dev/null +++ b/backend/app/api/routes/recipes/entity/user.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import List +from .....utils.pyobject_id import PyObjectId + + +class User(BaseModel): + id: PyObjectId = Field(alias='_id', default=None) + user_nickname: str + user_name: str + user_email: str + user_password: str + allergy: List[PyObjectId] = [] + recommend_history_by_model: List[PyObjectId] = [] + recommend_history_by_basket: List[PyObjectId] = [] + feedback_history: List[PyObjectId] = [] + initial_feedback_history: List[PyObjectId] = [] + + model_config = ConfigDict( + populate_by_name=True, + arbitrary_types_allowed=True, + json_schema_extra={ + "example": { + "id": "user_id", + "user_nickname": "johndoe", + "user_name": "John Doe", + "user_email": "john.doe@example.com", + "user_password": "secret_password", + "allergy": [], + "recommend_history_by_model": [], + "recommend_history_by_basket": [], + "feedback_history": [], + "initial_feedback_history": [], + } + }, + ) + + + def get_user_info(self) -> List[str]: + return [self.id, self.user_nickname, self.user_name, self.user_email, self.user_password, self.allergy, self.recommend_history_by_model, + self.recommend_history_by_basket, self.feedback_history, self.initial_feedback_history] + + def get_feedback_history(self) -> List[str]: + return self.feedback_history \ No newline at end of file From 7c32c5b6dd3ebc8a84a7cb3fead40b265e54be2c Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Wed, 13 Mar 2024 11:03:02 +0900 Subject: [PATCH 049/187] =?UTF-8?q?feat(repository):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=A0=91=EA=B7=BC=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../recipes/controller/recipes_controller.py | 4 +-- .../api/routes/recipes/repository/__init__.py | 0 .../recipes/repository/recipes_repository.py | 32 +++++++++++++++++++ .../routes/recipes/service/recipes_service.py | 6 ++-- 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 backend/app/api/routes/recipes/repository/__init__.py create mode 100644 backend/app/api/routes/recipes/repository/recipes_repository.py diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py index d7c2cd7..fe9e025 100644 --- a/backend/app/api/routes/recipes/controller/recipes_controller.py +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from ..service.recipes_service import get_user_cooked_recipes, get_recipes_by_recipes_id, get_ingredients_collections_by_recipes +from ..service.recipes_service import get_user_cooked_recipes, get_recipes_by_recipes_id, get_ingredients_list_by_recipes from ..dto.get_recipes_reponse_list import GetRecipesReponseList recipes_router = APIRouter() @@ -15,5 +15,5 @@ def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): # 레시피 id 리스트로 각 레시피의 정보 조회 recipes = get_recipes_by_recipes_id(user_cooked_recipes_id) # 레시피로 Ingredients 리스트 조회 - ingredients_collection = get_ingredients_collections_by_recipes(recipes) + ingredients_collection = get_ingredients_list_by_recipes(recipes) return GetRecipesReponseList(recipes, ingredients_collection) \ No newline at end of file diff --git a/backend/app/api/routes/recipes/repository/__init__.py b/backend/app/api/routes/recipes/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py new file mode 100644 index 0000000..d463a44 --- /dev/null +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -0,0 +1,32 @@ +from fastapi import HTTPException +from bson import ObjectId +from typing import List +from .....database.data_source import data_source +from ..entity.user import User +from ..entity.recipes import Recipes +from ..entity.ingredients import Ingredients + + +def select_user_by_user_id(user_id: str) -> User: + users_collection = data_source.collection_with_name_as("users") + user = users_collection.find_one({"_id": ObjectId(user_id)}) + if user: + return User(**user) + raise HTTPException(status_code=404, detail=f"User {id} not found") + + +def select_recipes_by_recipes_id(recipes_id: List[str]) -> Recipes: + recipes_collection = data_source.collection_with_name_as("recipes") + recipes = Recipes(recipes = recipes_collection.find({"_id": { "$in": list(map(ObjectId, recipes_id))} })) + if recipes: + return recipes + raise HTTPException(status_code=404, detail=f"Recipes not found") + + +def select_ingredients_by_ingredients_id(ingredients_id: List[str]) -> List[Recipes]: + ingredients_collection = data_source.collection_with_name_as("ingredients") + ingredients = Ingredients(ingredients = ingredients_collection.find({"_id": { "$in": list(map(ObjectId, ingredients_id))} })) + if ingredients: + return ingredients + raise HTTPException(status_code=404, detail=f"Ingredients not found") + diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py index 0b9a0df..42d4243 100644 --- a/backend/app/api/routes/recipes/service/recipes_service.py +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -15,10 +15,10 @@ def get_recipes_by_recipes_id(recipes_id: List[str]) -> Recipes: return recipes -def get_ingredients_collections_by_recipes(recipes: Recipes) -> List[Recipes]: +def get_ingredients_list_by_recipes(recipes: Recipes) -> List[Recipes]: # 레시피 별로 재료 리스트 조회: # [Ingredients(ingredients: [Ingredient, Ingredient]), Ingredients(ingredients: [Ingredient, Ingredient])] - ingredients_collection = [select_ingredients_by_ingredients_id(recipe.get_ingredients()) for recipe in recipes.get_recipes()] - return ingredients_collection + ingredients_list = [select_ingredients_by_ingredients_id(recipe.get_ingredients()) for recipe in recipes.get_recipes()] + return ingredients_list \ No newline at end of file From a32b5b7a9565da7234e2644349240fbda569f4db Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Wed, 13 Mar 2024 11:05:51 +0900 Subject: [PATCH 050/187] =?UTF-8?q?feat(utils):=20MongoDB=EC=9D=98=20Objec?= =?UTF-8?q?tID=20=EC=9E=90=EB=A3=8C=ED=98=95=20=EC=B2=98=EB=A6=AC=EB=A5=BC?= =?UTF-8?q?=20=EC=9C=84=ED=95=9C=20PyObjectId=20=EC=9E=90=EB=A3=8C?= =?UTF-8?q?=ED=98=95=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- backend/app/utils/__init__.py | 0 backend/app/utils/pyobject_id.py | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 backend/app/utils/__init__.py create mode 100644 backend/app/utils/pyobject_id.py diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/utils/pyobject_id.py b/backend/app/utils/pyobject_id.py new file mode 100644 index 0000000..8e8f933 --- /dev/null +++ b/backend/app/utils/pyobject_id.py @@ -0,0 +1,5 @@ +from pydantic.functional_validators import BeforeValidator +from typing_extensions import Annotated + + +PyObjectId = Annotated[str, BeforeValidator(str)] \ No newline at end of file From bfb06013e8cafbcde502aff6c1f2a7cfec3bc332 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Wed, 13 Mar 2024 11:08:19 +0900 Subject: [PATCH 051/187] =?UTF-8?q?feat(entity):=20Ingredient,=20Ingredien?= =?UTF-8?q?ts=20entity=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../api/routes/recipes/entity/ingredient.py | 28 +++++++++++++++++++ .../api/routes/recipes/entity/ingredients.py | 10 +++++++ 2 files changed, 38 insertions(+) create mode 100644 backend/app/api/routes/recipes/entity/ingredient.py create mode 100644 backend/app/api/routes/recipes/entity/ingredients.py diff --git a/backend/app/api/routes/recipes/entity/ingredient.py b/backend/app/api/routes/recipes/entity/ingredient.py new file mode 100644 index 0000000..5b850fc --- /dev/null +++ b/backend/app/api/routes/recipes/entity/ingredient.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field, ConfigDict +from .....utils.pyobject_id import PyObjectId + + +class Ingredient(BaseModel): + id: PyObjectId = Field(alias='_id', default=None) + name: str + price: float + price_url: str + + model_config = ConfigDict( + populate_by_name=True, + arbitrary_types_allowed=True, + json_schema_extra={ + "example": { + "id": "recipe_id", + "name": "김치", + "price": "5800", + "price_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + } + }, + ) + + def get_id(self): + return self.id + + def get_name(self): + return self.name diff --git a/backend/app/api/routes/recipes/entity/ingredients.py b/backend/app/api/routes/recipes/entity/ingredients.py new file mode 100644 index 0000000..3a34137 --- /dev/null +++ b/backend/app/api/routes/recipes/entity/ingredients.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel +from typing import List +from .ingredient import Ingredient + + +class Ingredients(BaseModel): + ingredients: List[Ingredient] + + def get_ingredients(self): + return self.ingredients \ No newline at end of file From 82523c8c097442ff2a50f34c566f682bf79131cb Mon Sep 17 00:00:00 2001 From: GangBean Date: Thu, 14 Mar 2024 15:17:49 +0900 Subject: [PATCH 052/187] =?UTF-8?q?docs:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B0=8F=20exception=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #11 --- .../users/controller/user_controller.py | 2 +- backend/app/app.py | 20 +++++++++++++++++++ backend/app/database/data_source.py | 2 +- backend/app/exception/__init__.py | 0 .../app/exception/users/signup_exeption.py | 14 +++++++++++++ backend/app/exception/users/user_exception.py | 3 +++ .../api/routes/users/controller/__init__.py | 0 .../test_user_controller.py} | 2 +- .../api/routes/users/repository/__init__.py | 0 .../users/repository/test_user_repository.py | 1 + 10 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 backend/app/app.py create mode 100644 backend/app/exception/__init__.py create mode 100644 backend/tests/api/routes/users/controller/__init__.py rename backend/tests/api/routes/users/{test_users.py => controller/test_user_controller.py} (99%) create mode 100644 backend/tests/api/routes/users/repository/__init__.py create mode 100644 backend/tests/api/routes/users/repository/test_user_repository.py diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 33b6cab..d211a3a 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -59,7 +59,7 @@ async def sign_up(request: SignupRequest) -> JSONResponse: password=request.password, nickname=request.nickname, email=request.email)) - return JSONResponse(content=response_body.model_dump(), status_code=status.HTTP_200_OK) +x return JSONResponse(content=response_body.model_dump(), status_code=status.HTTP_200_OK) @user_router.post('/api/users/auth') async def login(request: LoginRequest) -> Response: diff --git a/backend/app/app.py b/backend/app/app.py new file mode 100644 index 0000000..4ba583e --- /dev/null +++ b/backend/app/app.py @@ -0,0 +1,20 @@ +import uvicorn +from fastapi import FastAPI, APIRouter +from api.routes.users.controller.user_controller import user_router + +def new_app() -> FastAPI: + return FastAPI() + +app = new_app() + +router = APIRouter() + +@router.get('/') +def hello(): + return 'hello' + +app.include_router(router) +app.include_router(user_router) + +if __name__ == '__main__': + uvicorn.run(app, host='0.0.0.0') diff --git a/backend/app/database/data_source.py b/backend/app/database/data_source.py index 43163fa..c6808e1 100644 --- a/backend/app/database/data_source.py +++ b/backend/app/database/data_source.py @@ -7,7 +7,7 @@ from pydantic import BaseModel from typing import Optional -from ..exception.database.database_exception import ( +from exception.database.database_exception import ( DatabaseNotFoundException, CollectionNotFoundException ) diff --git a/backend/app/exception/__init__.py b/backend/app/exception/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/exception/users/signup_exeption.py b/backend/app/exception/users/signup_exeption.py index 05f0aea..a991dec 100644 --- a/backend/app/exception/users/signup_exeption.py +++ b/backend/app/exception/users/signup_exeption.py @@ -1,5 +1,19 @@ +from fastapi import status, Request +from fastapi.responses import JSONResponse + +from ...app import app from .user_exception import UserException +@app.exception_handler(UserException) +async def custom_exception_handler(request : Request, exc: UserException): + if isinstance(exc, UserSignUpLoginIdMissningException): + status_code = status.HTTP_400_BAD_REQUEST + content = {'message': exc.message} + return JSONResponse( + status_code=status_code, # 예외에 따라 상태 코드를 조정할 수 있습니다. + content=content + ) + class UserSingUpException(UserException): def __init__(self, message): super().__init__(message) diff --git a/backend/app/exception/users/user_exception.py b/backend/app/exception/users/user_exception.py index dbbcee8..6a8b859 100644 --- a/backend/app/exception/users/user_exception.py +++ b/backend/app/exception/users/user_exception.py @@ -1,6 +1,9 @@ +from ...app import app + class UserException(Exception): def __init__(self, message): super().__init__(message) def __str__(self): return self.message + diff --git a/backend/tests/api/routes/users/controller/__init__.py b/backend/tests/api/routes/users/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/users/test_users.py b/backend/tests/api/routes/users/controller/test_user_controller.py similarity index 99% rename from backend/tests/api/routes/users/test_users.py rename to backend/tests/api/routes/users/controller/test_user_controller.py index 008064e..74c386c 100644 --- a/backend/tests/api/routes/users/test_users.py +++ b/backend/tests/api/routes/users/controller/test_user_controller.py @@ -1,7 +1,7 @@ import pytest import requests -from .....app.exception.users.signup_exeption import ( +from ......app.exception.users.signup_exeption import ( UserSignUpLoginIdMissningException, UserSignUpPasswordMissningException, UserSignUpNicknameMissningException, UserSignUpEmailMissningException, UserSignUpInvalidLoginIdException, UserSignUpInvalidPasswordException, diff --git a/backend/tests/api/routes/users/repository/__init__.py b/backend/tests/api/routes/users/repository/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/users/repository/test_user_repository.py b/backend/tests/api/routes/users/repository/test_user_repository.py new file mode 100644 index 0000000..5871ed8 --- /dev/null +++ b/backend/tests/api/routes/users/repository/test_user_repository.py @@ -0,0 +1 @@ +import pytest From cfa527a8ede47d9ea0d7a24c9cfd22c02c7d0533 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 15:50:22 +0900 Subject: [PATCH 053/187] =?UTF-8?q?refactor(entity):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- backend/app/api/routes/recipes/entity/user.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/app/api/routes/recipes/entity/user.py b/backend/app/api/routes/recipes/entity/user.py index 3eb5c63..a09af22 100644 --- a/backend/app/api/routes/recipes/entity/user.py +++ b/backend/app/api/routes/recipes/entity/user.py @@ -34,10 +34,6 @@ class User(BaseModel): }, ) - - def get_user_info(self) -> List[str]: - return [self.id, self.user_nickname, self.user_name, self.user_email, self.user_password, self.allergy, self.recommend_history_by_model, - self.recommend_history_by_basket, self.feedback_history, self.initial_feedback_history] def get_feedback_history(self) -> List[str]: return self.feedback_history \ No newline at end of file From cd07afe750051ea29ffbde9366764ef33dbe4a42 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 15:52:10 +0900 Subject: [PATCH 054/187] =?UTF-8?q?feat(dto):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EC=A0=95=EB=B3=B4=20=EB=B0=98=ED=99=98=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20response=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../app/api/routes/recipes/dto/__init__.py | 0 .../routes/recipes/dto/get_recipes_reponse.py | 13 +++++++++ .../recipes/dto/get_recipes_reponse_list.py | 28 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 backend/app/api/routes/recipes/dto/__init__.py create mode 100644 backend/app/api/routes/recipes/dto/get_recipes_reponse.py create mode 100644 backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py diff --git a/backend/app/api/routes/recipes/dto/__init__.py b/backend/app/api/routes/recipes/dto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/recipes/dto/get_recipes_reponse.py b/backend/app/api/routes/recipes/dto/get_recipes_reponse.py new file mode 100644 index 0000000..49c8412 --- /dev/null +++ b/backend/app/api/routes/recipes/dto/get_recipes_reponse.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel +from .....utils.pyobject_id import PyObjectId +from typing import Dict + + +class GetRecipesReponse(BaseModel): + id: PyObjectId + recipe_name: str + ingredient: Dict[PyObjectId, str] = {} + recipe_url: str + recipe_img_url: str + + \ No newline at end of file diff --git a/backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py b/backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py new file mode 100644 index 0000000..94e59ef --- /dev/null +++ b/backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py @@ -0,0 +1,28 @@ +from ..entity.recipes import Recipes +from ..entity.ingredients import Ingredients +from .get_recipes_reponse import GetRecipesReponse +from typing import List + + +class GetRecipesReponseList: + response: List[GetRecipesReponse] + + def __init__(self, recipes: Recipes, ingredients_list: List[Ingredients]): + super().__init__() + self.response = list() + for recipe, ingredients in zip(recipes.get_recipes(), ingredients_list): + id = recipe.get_id() + recipe_name = recipe.get_recipe_name() + recipe_url = recipe.get_recipe_url() + response_ingredients = self._make_ingredient_dict(ingredients) + recipe_img_url = recipe.get_recipe_img_url() + self.response.append(GetRecipesReponse( + id = id, recipe_name=recipe_name, recipe_url=recipe_url, + ingredient=response_ingredients, recipe_img_url=recipe_img_url)) + + + def _make_ingredient_dict(self, ingredients: Ingredients): + result = {ingredient.get_id(): ingredient.get_name() for ingredient in ingredients.get_ingredients()} + return result + + \ No newline at end of file From 65687e9ccec05eca45db48d4e59388ce7fb6e99a Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 16:31:18 +0900 Subject: [PATCH 055/187] =?UTF-8?q?test(test):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- backend/app/api/routes/recipes/__init__.py | 0 backend/tests/api/__init__.py | 0 backend/tests/api/routes/__init__.py | 0 backend/tests/api/routes/recipes/__init__.py | 0 .../tests/api/routes/recipes/test_recipes.py | 110 ++++++++++++++++++ 5 files changed, 110 insertions(+) create mode 100644 backend/app/api/routes/recipes/__init__.py create mode 100644 backend/tests/api/__init__.py create mode 100644 backend/tests/api/routes/__init__.py create mode 100644 backend/tests/api/routes/recipes/__init__.py create mode 100644 backend/tests/api/routes/recipes/test_recipes.py diff --git a/backend/app/api/routes/recipes/__init__.py b/backend/app/api/routes/recipes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/__init__.py b/backend/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/__init__.py b/backend/tests/api/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/recipes/__init__.py b/backend/tests/api/routes/recipes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/api/routes/recipes/test_recipes.py b/backend/tests/api/routes/recipes/test_recipes.py new file mode 100644 index 0000000..a21ed29 --- /dev/null +++ b/backend/tests/api/routes/recipes/test_recipes.py @@ -0,0 +1,110 @@ +import pytest +from .....app.api.routes.recipes.repository.recipes_repository import select_user_by_user_id, select_recipes_by_recipes_id, select_ingredients_by_ingredients_id +from.....app.api.routes.recipes.entity.user import User +from.....app.api.routes.recipes.entity.recipes import Recipes +from.....app.api.routes.recipes.entity.recipe import Recipe + +from fastapi.testclient import TestClient +from .....app.api.routes.recipes.controller.recipes_controller import recipes_router + + +# User Type 확인 +@pytest.mark.parametrize("user_id, output", [ + ('65f0063d141b7b6fd385c7cc', True), +]) +def test_select_user_by_user_id_type(user_id, output): + user = select_user_by_user_id(user_id) + assert isinstance(user, User) == output + + +# user_id로 유저룰 알맞게 가져 오는지 테스트 +@pytest.mark.parametrize("user_id, output", [ + ('65f0063d141b7b6fd385c7cc', + { + "id": "65f0063d141b7b6fd385c7cc", + "user_nickname": "Son", + "user_name": "손흥민", + "user_email": "aaa@naver.com", + "user_password": "0000", + "allergy": [ + "65f01320141b7b6fd385c7d4", + "65f01320141b7b6fd385c7d4" + ], + "recommend_history_by_model": [ + "65f01320141b7b6fd385c7d4", + "65f01320141b7b6fd385c7d4" + ], + "recommend_history_by_basket": [ + "65f01320141b7b6fd385c7d4", + "65f01320141b7b6fd385c7d4" + ], + "feedback_history": [ + "65f0371e141b7b6fd385c7d8", + "65f29506141b7b6fd385c7e9" + ], + "initial_feedback_history": [ + "65f01320141b7b6fd385c7d4", + "65f01320141b7b6fd385c7d4" + ] + } + ), +]) +def test_select_user_by_user_id(user_id, output): + user = select_user_by_user_id(user_id) + assert user.model_dump() == output + + +# feedback list로 레시피를 올바르게 조회하는지 테스트 +@pytest.mark.parametrize("recipes_id, output", [ + (["65f0371e141b7b6fd385c7d8", "65f0371e141b7b6fd385c7d8"], True), +]) +def test_select_recipes_by_recipes_id(recipes_id, output): + recipes = select_recipes_by_recipes_id(recipes_id) + assert isinstance(recipes, Recipes) == output + assert isinstance(recipes.get_recipes()[0], Recipe) == output + + +# 레시피에서 재료 올바르게 조회하는지 테스트 +@pytest.mark.parametrize("ingredients_id, output", [ + (["65f04741141b7b6fd385c7da", "65f047b9141b7b6fd385c7db"], []), +]) +def test_select_ingredients_by_ingredients_id(ingredients_id, output): + recipes = select_ingredients_by_ingredients_id(ingredients_id) + + +# API 테스트 +client = TestClient(recipes_router) + +@pytest.mark.parametrize("user_id, output", [ + ("65f0063d141b7b6fd385c7cc", + { + "response": [ + { + "id": "65f0371e141b7b6fd385c7d8", + "recipe_name": "매콤한 김치찌개", + "ingredient": { + "65f04741141b7b6fd385c7da": "김치", + "65f047b9141b7b6fd385c7db": "삼겹살" + }, + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + }, + { + "id": "65f29506141b7b6fd385c7e9", + "recipe_name": "맛있는 제육볶음", + "ingredient": { + "65f29547141b7b6fd385c7eb": "고추장", + "65f2955e141b7b6fd385c7f1": "양파" + }, + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + } + ] +} + ), +]) +def test_read_item(user_id, output): + response = client.get(f"/users/{user_id}/recipes/cooked") + assert response.status_code == 200 + assert response.json() == output + From 4323e17418c59ae98a5168e67c5315ecbc9099b9 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 16:51:52 +0900 Subject: [PATCH 056/187] =?UTF-8?q?fix(controller):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EA=B0=92=20=ED=83=80=EC=9E=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../api/routes/recipes/controller/recipes_controller.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py index fe9e025..c1fa0bc 100644 --- a/backend/app/api/routes/recipes/controller/recipes_controller.py +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -6,8 +6,7 @@ @recipes_router.get( "/users/{user_id}/recipes/cooked", - response_description="유저가 요리한 레시피 목록", - response_model=GetRecipesReponseList + response_description="유저가 요리한 레시피 목록" ) def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): # user_id로 유저가 요리한 레시피 id 리스트 조회 @@ -15,5 +14,5 @@ def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): # 레시피 id 리스트로 각 레시피의 정보 조회 recipes = get_recipes_by_recipes_id(user_cooked_recipes_id) # 레시피로 Ingredients 리스트 조회 - ingredients_collection = get_ingredients_list_by_recipes(recipes) - return GetRecipesReponseList(recipes, ingredients_collection) \ No newline at end of file + ingredients_list = get_ingredients_list_by_recipes(recipes) + return GetRecipesReponseList(recipes, ingredients_list) \ No newline at end of file From 88bf840bb653f22ec67de1e81c88e59d9b735d85 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 17:38:23 +0900 Subject: [PATCH 057/187] =?UTF-8?q?fix(repository):=20select=5Fingredients?= =?UTF-8?q?=5Fby=5Fingredients=5Fid=20=ED=95=A8=EC=88=98=EC=9D=98=20return?= =?UTF-8?q?=20=EA=B0=92=20type=20hint=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- backend/app/api/routes/recipes/repository/recipes_repository.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index d463a44..89c9072 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -23,7 +23,7 @@ def select_recipes_by_recipes_id(recipes_id: List[str]) -> Recipes: raise HTTPException(status_code=404, detail=f"Recipes not found") -def select_ingredients_by_ingredients_id(ingredients_id: List[str]) -> List[Recipes]: +def select_ingredients_by_ingredients_id(ingredients_id: List[str]) -> Ingredients: ingredients_collection = data_source.collection_with_name_as("ingredients") ingredients = Ingredients(ingredients = ingredients_collection.find({"_id": { "$in": list(map(ObjectId, ingredients_id))} })) if ingredients: From ac51e4d58cbecd83f0f728a6533a8612ce0459e3 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 17:38:56 +0900 Subject: [PATCH 058/187] =?UTF-8?q?test(test):=20=EC=9E=AC=EB=A3=8C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../tests/api/routes/recipes/test_recipes.py | 85 +++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/backend/tests/api/routes/recipes/test_recipes.py b/backend/tests/api/routes/recipes/test_recipes.py index a21ed29..673fd26 100644 --- a/backend/tests/api/routes/recipes/test_recipes.py +++ b/backend/tests/api/routes/recipes/test_recipes.py @@ -3,6 +3,8 @@ from.....app.api.routes.recipes.entity.user import User from.....app.api.routes.recipes.entity.recipes import Recipes from.....app.api.routes.recipes.entity.recipe import Recipe +from.....app.api.routes.recipes.entity.ingredients import Ingredients +from.....app.api.routes.recipes.entity.ingredient import Ingredient from fastapi.testclient import TestClient from .....app.api.routes.recipes.controller.recipes_controller import recipes_router @@ -54,22 +56,93 @@ def test_select_user_by_user_id(user_id, output): assert user.model_dump() == output -# feedback list로 레시피를 올바르게 조회하는지 테스트 +# feedback list로 조회한 레시피 타입 확인 @pytest.mark.parametrize("recipes_id, output", [ - (["65f0371e141b7b6fd385c7d8", "65f0371e141b7b6fd385c7d8"], True), + (["65f0371e141b7b6fd385c7d8", "65f29506141b7b6fd385c7e9"], True), ]) -def test_select_recipes_by_recipes_id(recipes_id, output): +def test_select_recipes_by_recipes_id_type(recipes_id, output): recipes = select_recipes_by_recipes_id(recipes_id) assert isinstance(recipes, Recipes) == output assert isinstance(recipes.get_recipes()[0], Recipe) == output + + +# feedback list로 레시피를 올바르게 조회하는지 테스트 +@pytest.mark.parametrize("recipes_id, output", [ + (["65f0371e141b7b6fd385c7d8", "65f29506141b7b6fd385c7e9"], + { + "recipes": [ + { + "id": "65f0371e141b7b6fd385c7d8", + "food_name": "김치찌개", + "recipe_name": "매콤한 김치찌개", + "ingredient": [ + "65f04741141b7b6fd385c7da", + "65f047b9141b7b6fd385c7db" + ], + "time_taken": 30, + "difficulty": "초급", + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "portion": "4인분", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + }, + { + "id": "65f29506141b7b6fd385c7e9", + "food_name": "제육볶음", + "recipe_name": "맛있는 제육볶음", + "ingredient": [ + "65f29547141b7b6fd385c7eb", + "65f2955e141b7b6fd385c7f1" + ], + "time_taken": 30, + "difficulty": "중급", + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "portion": "2인분", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + } + ] + } + ), +]) +def test_select_recipes_by_recipes_id(recipes_id, output): + recipes = select_recipes_by_recipes_id(recipes_id) + assert recipes.model_dump() == output + + +# 재료 타입 확인 +@pytest.mark.parametrize("ingredients_id, output", [ + (["65f29547141b7b6fd385c7eb", "65f2955e141b7b6fd385c7f1"], True), +]) +def test_select_ingredients_by_ingredients_id_type(ingredients_id, output): + ingredients = select_ingredients_by_ingredients_id(ingredients_id) + assert isinstance(ingredients, Ingredients) == output + assert isinstance(ingredients.get_ingredients()[0], Ingredient) == output -# 레시피에서 재료 올바르게 조회하는지 테스트 + +# 재료를 올바르게 조회하는지 확인 @pytest.mark.parametrize("ingredients_id, output", [ - (["65f04741141b7b6fd385c7da", "65f047b9141b7b6fd385c7db"], []), + (["65f04741141b7b6fd385c7da", "65f047b9141b7b6fd385c7db"], + { + "ingredients": [ + { + "id": "65f04741141b7b6fd385c7da", + "name": "김치", + "price": 5800.0, + "price_url": "https://a" + }, + { + "id": "65f047b9141b7b6fd385c7db", + "name": "삼겹살", + "price": 15800.0, + "price_url": "https://a" + } + ] + } + ), ]) def test_select_ingredients_by_ingredients_id(ingredients_id, output): - recipes = select_ingredients_by_ingredients_id(ingredients_id) + ingredients = select_ingredients_by_ingredients_id(ingredients_id) + assert ingredients.model_dump() == output # API 테스트 From 05209652651653a4658b56997e31a25f66cb85c4 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 17:56:22 +0900 Subject: [PATCH 059/187] =?UTF-8?q?feat(service):=20user=5Fid=EB=A1=9C=20r?= =?UTF-8?q?ecipes=5Fid=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EB=A1=9C=EC=A7=81=EC=9D=84=20user=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=99=80=20recipes=5Fid=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../routes/recipes/service/recipes_service.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py index 42d4243..dcf8e1d 100644 --- a/backend/app/api/routes/recipes/service/recipes_service.py +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -1,12 +1,22 @@ from typing import List from ..repository.recipes_repository import select_user_by_user_id, select_recipes_by_recipes_id, select_ingredients_by_ingredients_id from ..entity.recipes import Recipes +from ..entity.user import User -def get_user_cooked_recipes(user_id: str) -> List[str]: +def get_user_by_user_id(user_id: str) -> User: user = select_user_by_user_id(user_id) - user_cooked_recipes = user.get_feedback_history() - return user_cooked_recipes + return user + + +def get_user_cooked_recipes_id_by_user(user: User) -> List[str]: + user_cooked_recipes_id = user.get_feedback_history() + return user_cooked_recipes_id + + +def get_user_recommended_recipes_id_by_user(user: User) -> List[str]: + user_recommended_recipes_id = user.get_recommend_history_by_basket() + return user_recommended_recipes_id def get_recipes_by_recipes_id(recipes_id: List[str]) -> Recipes: From 8b030d6f4ca74dab49e6d3683f7940f174d6a2c6 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 18:04:41 +0900 Subject: [PATCH 060/187] =?UTF-8?q?fix(dto):=20API=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=97=90=20=EC=9C=A0=EC=A0=80=EA=B0=80=20=EC=9A=94=EB=A6=AC?= =?UTF-8?q?=ED=95=9C=20recipes=5Fid=EC=99=80=20next=5Fpage=5Furl=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../recipes/dto/get_recipes_reponse_list.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py b/backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py index 94e59ef..51219e6 100644 --- a/backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py +++ b/backend/app/api/routes/recipes/dto/get_recipes_reponse_list.py @@ -5,21 +5,25 @@ class GetRecipesReponseList: - response: List[GetRecipesReponse] - - def __init__(self, recipes: Recipes, ingredients_list: List[Ingredients]): + recipe_list: List[GetRecipesReponse] + cooked_recipes_id: List[str] + next_page_url: str + + def __init__(self, recipes: Recipes, ingredients_list: List[Ingredients], cooked_recipes_id: List[str] = None, next_page_url: str = None): super().__init__() - self.response = list() + self.recipe_list = list() for recipe, ingredients in zip(recipes.get_recipes(), ingredients_list): id = recipe.get_id() recipe_name = recipe.get_recipe_name() recipe_url = recipe.get_recipe_url() response_ingredients = self._make_ingredient_dict(ingredients) recipe_img_url = recipe.get_recipe_img_url() - self.response.append(GetRecipesReponse( + self.recipe_list.append(GetRecipesReponse( id = id, recipe_name=recipe_name, recipe_url=recipe_url, ingredient=response_ingredients, recipe_img_url=recipe_img_url)) - + self.cooked_recipes_id = cooked_recipes_id + self.next_page_url = next_page_url + def _make_ingredient_dict(self, ingredients: Ingredients): result = {ingredient.get_id(): ingredient.get_name() for ingredient in ingredients.get_ingredients()} From dff9dbbc481963a68e818cbf47ef455205af6953 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 18:05:40 +0900 Subject: [PATCH 061/187] =?UTF-8?q?fix(entity):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=B6=94=EC=B2=9C=EB=B0=9B=EC=9D=80=20=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- backend/app/api/routes/recipes/entity/user.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/entity/user.py b/backend/app/api/routes/recipes/entity/user.py index a09af22..40d7286 100644 --- a/backend/app/api/routes/recipes/entity/user.py +++ b/backend/app/api/routes/recipes/entity/user.py @@ -36,4 +36,7 @@ class User(BaseModel): def get_feedback_history(self) -> List[str]: - return self.feedback_history \ No newline at end of file + return self.feedback_history + + def get_recommend_history_by_basket(self) -> List[str]: + return self.recommend_history_by_basket \ No newline at end of file From f9994a42b9ec3b2fc97a5d6087fd03769918bd54 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 18:07:22 +0900 Subject: [PATCH 062/187] =?UTF-8?q?feat(controller):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=B6=94=EC=B2=9C=20=EB=B0=9B=EC=9D=80=20=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../recipes/controller/recipes_controller.py | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py index c1fa0bc..7867d45 100644 --- a/backend/app/api/routes/recipes/controller/recipes_controller.py +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -1,7 +1,8 @@ from fastapi import APIRouter -from ..service.recipes_service import get_user_cooked_recipes, get_recipes_by_recipes_id, get_ingredients_list_by_recipes +from ..service.recipes_service import * from ..dto.get_recipes_reponse_list import GetRecipesReponseList + recipes_router = APIRouter() @recipes_router.get( @@ -9,10 +10,29 @@ response_description="유저가 요리한 레시피 목록" ) def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): - # user_id로 유저가 요리한 레시피 id 리스트 조회 - user_cooked_recipes_id = get_user_cooked_recipes(user_id) + # user_id로 유저 조회 + user = get_user_by_user_id(user_id) + # user로 유저가 요리한 레시피 id 리스트 조회 + user_cooked_recipes_id = get_user_cooked_recipes_id_by_user(user) # 레시피 id 리스트로 각 레시피의 정보 조회 recipes = get_recipes_by_recipes_id(user_cooked_recipes_id) # 레시피로 Ingredients 리스트 조회 ingredients_list = get_ingredients_list_by_recipes(recipes) - return GetRecipesReponseList(recipes, ingredients_list) \ No newline at end of file + return GetRecipesReponseList(recipes, ingredients_list) + + +@recipes_router.get( + "/users/{user_id}/recipes/recommended", + response_description="유저가 추천받은 레시피 목록" + ) +def get_user_recommended_recipes_by_page(user_id: str, page_num: int = 0): + # user_id로 유저가 추천 받은 레시피 id 리스트 조회 + user = get_user_by_user_id(user_id) + user_cooked_recipes_id = get_user_cooked_recipes_id_by_user(user) + user_recommended_recipes_id = get_user_recommended_recipes_id_by_user(user) + # 레시피 id 리스트로 각 레시피의 정보 조회 + recipes = get_recipes_by_recipes_id(user_cooked_recipes_id + user_recommended_recipes_id) + # 레시피로 Ingredients 리스트 조회 + ingredients_list = get_ingredients_list_by_recipes(recipes) + # 유저가 요리한 요리 표시를 위한 user_cooked_recipes_id 함께 넘기기 + return GetRecipesReponseList(recipes, ingredients_list, user_cooked_recipes_id) \ No newline at end of file From a0fd567d9d1f8215a01004f138f8995ec0b066a6 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 18:24:55 +0900 Subject: [PATCH 063/187] =?UTF-8?q?refactor(controller):=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=EA=B0=80=20=EC=B6=94=EC=B2=9C=20=EB=B0=9B=EC=9D=80=20?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=94=BC=EC=99=80=EC=99=80=20=EC=9A=94?= =?UTF-8?q?=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C=ED=94=BC=EC=9D=98=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EB=94=94=EB=A5=BC=20recipes=5Fid=EC=97=90=20?= =?UTF-8?q?=ED=95=A8=EA=BB=98=20=EC=A0=80=EC=9E=A5=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../app/api/routes/recipes/controller/recipes_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py index 7867d45..1df0c16 100644 --- a/backend/app/api/routes/recipes/controller/recipes_controller.py +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -31,7 +31,8 @@ def get_user_recommended_recipes_by_page(user_id: str, page_num: int = 0): user_cooked_recipes_id = get_user_cooked_recipes_id_by_user(user) user_recommended_recipes_id = get_user_recommended_recipes_id_by_user(user) # 레시피 id 리스트로 각 레시피의 정보 조회 - recipes = get_recipes_by_recipes_id(user_cooked_recipes_id + user_recommended_recipes_id) + recipes_id = user_cooked_recipes_id + user_recommended_recipes_id + recipes = get_recipes_by_recipes_id(recipes_id) # 레시피로 Ingredients 리스트 조회 ingredients_list = get_ingredients_list_by_recipes(recipes) # 유저가 요리한 요리 표시를 위한 user_cooked_recipes_id 함께 넘기기 From 8d60b83be835b3c5e53ea373aa351ae095a3bfce Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Thu, 14 Mar 2024 18:26:36 +0900 Subject: [PATCH 064/187] =?UTF-8?q?test(test):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=B6=94=EC=B2=9C=20=EB=B0=9B=EC=9D=80=20=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 21 --- .../tests/api/routes/recipes/test_recipes.py | 105 +++++++++++++----- 1 file changed, 77 insertions(+), 28 deletions(-) diff --git a/backend/tests/api/routes/recipes/test_recipes.py b/backend/tests/api/routes/recipes/test_recipes.py index 673fd26..d78c349 100644 --- a/backend/tests/api/routes/recipes/test_recipes.py +++ b/backend/tests/api/routes/recipes/test_recipes.py @@ -37,8 +37,7 @@ def test_select_user_by_user_id_type(user_id, output): "65f01320141b7b6fd385c7d4" ], "recommend_history_by_basket": [ - "65f01320141b7b6fd385c7d4", - "65f01320141b7b6fd385c7d4" + "65f2bef7141b7b6fd385c810", ], "feedback_history": [ "65f0371e141b7b6fd385c7d8", @@ -150,34 +149,84 @@ def test_select_ingredients_by_ingredients_id(ingredients_id, output): @pytest.mark.parametrize("user_id, output", [ ("65f0063d141b7b6fd385c7cc", - { - "response": [ - { - "id": "65f0371e141b7b6fd385c7d8", - "recipe_name": "매콤한 김치찌개", - "ingredient": { - "65f04741141b7b6fd385c7da": "김치", - "65f047b9141b7b6fd385c7db": "삼겹살" - }, - "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", - "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" - }, - { - "id": "65f29506141b7b6fd385c7e9", - "recipe_name": "맛있는 제육볶음", - "ingredient": { - "65f29547141b7b6fd385c7eb": "고추장", - "65f2955e141b7b6fd385c7f1": "양파" - }, - "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", - "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" - } - ] -} - ), + { + "recipe_list": [ + { + "id": "65f0371e141b7b6fd385c7d8", + "recipe_name": "매콤한 김치찌개", + "ingredient": { + "65f04741141b7b6fd385c7da": "김치", + "65f047b9141b7b6fd385c7db": "삼겹살" + }, + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + }, + { + "id": "65f29506141b7b6fd385c7e9", + "recipe_name": "맛있는 제육볶음", + "ingredient": { + "65f29547141b7b6fd385c7eb": "고추장", + "65f2955e141b7b6fd385c7f1": "양파" + }, + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + } + ], + "cooked_recipes_id": None, + "next_page_url": None + } + ), ]) -def test_read_item(user_id, output): +def test_get_user_cooked_recipes_by_page(user_id, output): response = client.get(f"/users/{user_id}/recipes/cooked") assert response.status_code == 200 assert response.json() == output + + +@pytest.mark.parametrize("user_id, output", [ + ("65f0063d141b7b6fd385c7cc", + { + "recipe_list": [ + { + "id": "65f0371e141b7b6fd385c7d8", + "recipe_name": "매콤한 김치찌개", + "ingredient": { + "65f04741141b7b6fd385c7da": "김치", + "65f047b9141b7b6fd385c7db": "삼겹살" + }, + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + }, + { + "id": "65f29506141b7b6fd385c7e9", + "recipe_name": "맛있는 제육볶음", + "ingredient": { + "65f29547141b7b6fd385c7eb": "고추장", + "65f2955e141b7b6fd385c7f1": "양파" + }, + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + }, + { + "id": "65f2bef7141b7b6fd385c810", + "recipe_name": "달콤한 불고기", + "ingredient": { + "65f2955e141b7b6fd385c7f1": "양파" + }, + "recipe_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2019/03/10/ad0e61fd8b4783a926ebccadd0c1b8c11.jpg" + } + ], + "cooked_recipes_id": [ + "65f0371e141b7b6fd385c7d8", + "65f29506141b7b6fd385c7e9" + ], + "next_page_url": None + } + ), +]) +def test_get_user_recommended_recipes_by_page(user_id, output): + response = client.get(f"/users/{user_id}/recipes/recommended/") + assert response.status_code == 200 + assert response.json() == output From f870eda3b1ca1538441ee2f68f1bf0f99a917c5e Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 00:16:26 +0900 Subject: [PATCH 065/187] =?UTF-8?q?feat(service):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- .../routes/recipes/service/recipes_service.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py index dcf8e1d..7142390 100644 --- a/backend/app/api/routes/recipes/service/recipes_service.py +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -1,5 +1,5 @@ from typing import List -from ..repository.recipes_repository import select_user_by_user_id, select_recipes_by_recipes_id, select_ingredients_by_ingredients_id +from ..repository.recipes_repository import select_user_by_user_id, select_recipes_by_recipes_id, select_ingredients_by_ingredients_id, update_cooked_recipes from ..entity.recipes import Recipes from ..entity.user import User @@ -30,5 +30,17 @@ def get_ingredients_list_by_recipes(recipes: Recipes) -> List[Recipes]: # [Ingredients(ingredients: [Ingredient, Ingredient]), Ingredients(ingredients: [Ingredient, Ingredient])] ingredients_list = [select_ingredients_by_ingredients_id(recipe.get_ingredients()) for recipe in recipes.get_recipes()] return ingredients_list - + + +def update_cooked_recipes(user_id: str, recipes_id: str, feedback: bool): + user = get_user_by_user_id(user_id) + user_cooked_recipes_id = get_user_cooked_recipes_id_by_user(user) + user_recommended_recipes_id = get_user_recommended_recipes_id_by_user(user) + if feedback: + user_recommended_recipes_id.remove(recipes_id) + user_cooked_recipes_id.append(recipes_id) + return update_cooked_recipes(user_id, user_cooked_recipes_id, user_recommended_recipes_id) + user_cooked_recipes_id.remove(recipes_id) + user_recommended_recipes_id.append(recipes_id) + return update_cooked_recipes(user_id, user_cooked_recipes_id, user_recommended_recipes_id) \ No newline at end of file From 25c7a3b3f83283876e154b3cfdc08a56cbb2e3f4 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 00:28:02 +0900 Subject: [PATCH 066/187] =?UTF-8?q?feat(repository):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- .../recipes/repository/recipes_repository.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index 89c9072..f7f1015 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -6,9 +6,10 @@ from ..entity.recipes import Recipes from ..entity.ingredients import Ingredients +users_collection = data_source.collection_with_name_as("users") + def select_user_by_user_id(user_id: str) -> User: - users_collection = data_source.collection_with_name_as("users") user = users_collection.find_one({"_id": ObjectId(user_id)}) if user: return User(**user) @@ -30,3 +31,14 @@ def select_ingredients_by_ingredients_id(ingredients_id: List[str]) -> Ingredien return ingredients raise HTTPException(status_code=404, detail=f"Ingredients not found") + +def update_cooked_recipes(user_id: str, user_cooked_recipes_id: List[str], user_recommended_recipes_id: List[str]) -> bool: + update_result = users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$set": { + "feedback_history": user_cooked_recipes_id, + "recommend_history_by_basket": user_recommended_recipes_id + }} + ) + return update_result.modified_count > 0 + From 50d612067818d8109d320d695f81c596a9647737 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 00:28:51 +0900 Subject: [PATCH 067/187] =?UTF-8?q?feat(dto):=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=84=20=EC=9C=84=ED=95=9C=20dto=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- .../api/routes/recipes/dto/recipe_status_upadate_request.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 backend/app/api/routes/recipes/dto/recipe_status_upadate_request.py diff --git a/backend/app/api/routes/recipes/dto/recipe_status_upadate_request.py b/backend/app/api/routes/recipes/dto/recipe_status_upadate_request.py new file mode 100644 index 0000000..5a069d8 --- /dev/null +++ b/backend/app/api/routes/recipes/dto/recipe_status_upadate_request.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class RecipeStatusUpadateRequest(BaseModel): + feedback: bool \ No newline at end of file From 36b167b07d130312212e9bf6fd6c385d48ec92b3 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 00:30:15 +0900 Subject: [PATCH 068/187] =?UTF-8?q?feat(controller):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- .../recipes/controller/recipes_controller.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py index 1df0c16..7c35b91 100644 --- a/backend/app/api/routes/recipes/controller/recipes_controller.py +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -1,6 +1,7 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Response, HTTPException from ..service.recipes_service import * from ..dto.get_recipes_reponse_list import GetRecipesReponseList +from ..dto.recipe_status_upadate_request import RecipeStatusUpadateRequest recipes_router = APIRouter() @@ -36,4 +37,13 @@ def get_user_recommended_recipes_by_page(user_id: str, page_num: int = 0): # 레시피로 Ingredients 리스트 조회 ingredients_list = get_ingredients_list_by_recipes(recipes) # 유저가 요리한 요리 표시를 위한 user_cooked_recipes_id 함께 넘기기 - return GetRecipesReponseList(recipes, ingredients_list, user_cooked_recipes_id) \ No newline at end of file + return GetRecipesReponseList(recipes, ingredients_list, user_cooked_recipes_id) + + +@recipes_router.patch("/users/{user_id}/recipes/{recipes_id}/feedback") +def update_user_recipes_status__by_feedback(user_id: str, recipes_id: str, request: RecipeStatusUpadateRequest): + update_result = update_cooked_recipes(user_id, recipes_id, request.feedback) + if update_result: + return Response(status_code=200) + raise HTTPException(status_code=404, detail="User not found") + From 4620becd6153c42c8a4beeeb1f1abbba2024de89 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 00:48:50 +0900 Subject: [PATCH 069/187] =?UTF-8?q?refactor(repository):=20RecipesReposito?= =?UTF-8?q?ry=EB=A5=BC=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- .../recipes/repository/recipes_repository.py | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index f7f1015..4611dbb 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -6,39 +6,42 @@ from ..entity.recipes import Recipes from ..entity.ingredients import Ingredients -users_collection = data_source.collection_with_name_as("users") - - -def select_user_by_user_id(user_id: str) -> User: - user = users_collection.find_one({"_id": ObjectId(user_id)}) - if user: - return User(**user) - raise HTTPException(status_code=404, detail=f"User {id} not found") - - -def select_recipes_by_recipes_id(recipes_id: List[str]) -> Recipes: - recipes_collection = data_source.collection_with_name_as("recipes") - recipes = Recipes(recipes = recipes_collection.find({"_id": { "$in": list(map(ObjectId, recipes_id))} })) - if recipes: - return recipes - raise HTTPException(status_code=404, detail=f"Recipes not found") - - -def select_ingredients_by_ingredients_id(ingredients_id: List[str]) -> Ingredients: - ingredients_collection = data_source.collection_with_name_as("ingredients") - ingredients = Ingredients(ingredients = ingredients_collection.find({"_id": { "$in": list(map(ObjectId, ingredients_id))} })) - if ingredients: - return ingredients - raise HTTPException(status_code=404, detail=f"Ingredients not found") - - -def update_cooked_recipes(user_id: str, user_cooked_recipes_id: List[str], user_recommended_recipes_id: List[str]) -> bool: - update_result = users_collection.update_one( - {"_id": ObjectId(user_id)}, - {"$set": { - "feedback_history": user_cooked_recipes_id, - "recommend_history_by_basket": user_recommended_recipes_id - }} - ) - return update_result.modified_count > 0 - + +class RecipesRepository: + def __init__(self): + self.users_collection = data_source.collection_with_name_as("users") + self.recipes_collection = data_source.collection_with_name_as("recipes") + self.ingredients_collection = data_source.collection_with_name_as("ingredients") + + + def select_user_by_user_id(self, user_id: str) -> User: + user = self.users_collection.find_one({"_id": ObjectId(user_id)}) + if user: + return User(**user) + raise HTTPException(status_code=404, detail=f"User {id} not found") + + + def select_recipes_by_recipes_id(self, recipes_id: List[str]) -> Recipes: + recipes = Recipes(recipes = self.recipes_collection.find({"_id": { "$in": list(map(ObjectId, recipes_id))} })) + if recipes: + return recipes + raise HTTPException(status_code=404, detail=f"Recipes not found") + + + def select_ingredients_by_ingredients_id(self, ingredients_id: List[str]) -> Ingredients: + ingredients = Ingredients(ingredients = self.ingredients_collection.find({"_id": { "$in": list(map(ObjectId, ingredients_id))} })) + if ingredients: + return ingredients + raise HTTPException(status_code=404, detail=f"Ingredients not found") + + + def update_cooked_recipes(self, user_id: str, user_cooked_recipes_id: List[str], user_recommended_recipes_id: List[str]) -> bool: + update_result = self.users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$set": { + "feedback_history": user_cooked_recipes_id, + "recommend_history_by_basket": user_recommended_recipes_id + }} + ) + return update_result.modified_count > 0 + From 883e285a517ee90cd0c87ddb204111dfab2a604a Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 00:50:35 +0900 Subject: [PATCH 070/187] =?UTF-8?q?refactor(service):=20RecipesService?= =?UTF-8?q?=EB=A5=BC=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- .../recipes/controller/recipes_controller.py | 23 +++--- .../routes/recipes/service/recipes_service.py | 70 +++++++++++-------- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py index 7c35b91..fd48361 100644 --- a/backend/app/api/routes/recipes/controller/recipes_controller.py +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -1,10 +1,11 @@ from fastapi import APIRouter, Response, HTTPException -from ..service.recipes_service import * +from ..service.recipes_service import RecipesService from ..dto.get_recipes_reponse_list import GetRecipesReponseList from ..dto.recipe_status_upadate_request import RecipeStatusUpadateRequest recipes_router = APIRouter() +recipes_service = RecipesService() @recipes_router.get( "/users/{user_id}/recipes/cooked", @@ -12,13 +13,13 @@ ) def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): # user_id로 유저 조회 - user = get_user_by_user_id(user_id) + user = recipes_service.get_user_by_user_id(user_id) # user로 유저가 요리한 레시피 id 리스트 조회 - user_cooked_recipes_id = get_user_cooked_recipes_id_by_user(user) + user_cooked_recipes_id = recipes_service.get_user_cooked_recipes_id_by_user(user) # 레시피 id 리스트로 각 레시피의 정보 조회 - recipes = get_recipes_by_recipes_id(user_cooked_recipes_id) + recipes = recipes_service.get_recipes_by_recipes_id(user_cooked_recipes_id) # 레시피로 Ingredients 리스트 조회 - ingredients_list = get_ingredients_list_by_recipes(recipes) + ingredients_list = recipes_service.get_ingredients_list_by_recipes(recipes) return GetRecipesReponseList(recipes, ingredients_list) @@ -28,21 +29,21 @@ def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): ) def get_user_recommended_recipes_by_page(user_id: str, page_num: int = 0): # user_id로 유저가 추천 받은 레시피 id 리스트 조회 - user = get_user_by_user_id(user_id) - user_cooked_recipes_id = get_user_cooked_recipes_id_by_user(user) - user_recommended_recipes_id = get_user_recommended_recipes_id_by_user(user) + user = recipes_service.get_user_by_user_id(user_id) + user_cooked_recipes_id = recipes_service.get_user_cooked_recipes_id_by_user(user) + user_recommended_recipes_id = recipes_service.get_user_recommended_recipes_id_by_user(user) # 레시피 id 리스트로 각 레시피의 정보 조회 recipes_id = user_cooked_recipes_id + user_recommended_recipes_id - recipes = get_recipes_by_recipes_id(recipes_id) + recipes = recipes_service.get_recipes_by_recipes_id(recipes_id) # 레시피로 Ingredients 리스트 조회 - ingredients_list = get_ingredients_list_by_recipes(recipes) + ingredients_list = recipes_service.get_ingredients_list_by_recipes(recipes) # 유저가 요리한 요리 표시를 위한 user_cooked_recipes_id 함께 넘기기 return GetRecipesReponseList(recipes, ingredients_list, user_cooked_recipes_id) @recipes_router.patch("/users/{user_id}/recipes/{recipes_id}/feedback") def update_user_recipes_status__by_feedback(user_id: str, recipes_id: str, request: RecipeStatusUpadateRequest): - update_result = update_cooked_recipes(user_id, recipes_id, request.feedback) + update_result = recipes_service.update_cooked_recipes(user_id, recipes_id, request.feedback) if update_result: return Response(status_code=200) raise HTTPException(status_code=404, detail="User not found") diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py index 7142390..269b228 100644 --- a/backend/app/api/routes/recipes/service/recipes_service.py +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -1,46 +1,54 @@ from typing import List -from ..repository.recipes_repository import select_user_by_user_id, select_recipes_by_recipes_id, select_ingredients_by_ingredients_id, update_cooked_recipes +from ..repository.recipes_repository import RecipesRepository from ..entity.recipes import Recipes from ..entity.user import User -def get_user_by_user_id(user_id: str) -> User: - user = select_user_by_user_id(user_id) - return user +class RecipesService: + def __init__(self, recipes_repository: RecipesRepository = None): + self.recipes_repository = RecipesRepository() + + + def get_user_by_user_id(self, user_id: str) -> User: + user = self.recipes_repository.select_user_by_user_id(user_id) + return user -def get_user_cooked_recipes_id_by_user(user: User) -> List[str]: - user_cooked_recipes_id = user.get_feedback_history() - return user_cooked_recipes_id + def get_user_cooked_recipes_id_by_user(self, user: User) -> List[str]: + user_cooked_recipes_id = user.get_feedback_history() + return user_cooked_recipes_id -def get_user_recommended_recipes_id_by_user(user: User) -> List[str]: - user_recommended_recipes_id = user.get_recommend_history_by_basket() - return user_recommended_recipes_id + def get_user_recommended_recipes_id_by_user(self, user: User) -> List[str]: + user_recommended_recipes_id = user.get_recommend_history_by_basket() + return user_recommended_recipes_id -def get_recipes_by_recipes_id(recipes_id: List[str]) -> Recipes: - # 레시피 리스트 조회 - recipes = select_recipes_by_recipes_id(recipes_id) - return recipes + def get_recipes_by_recipes_id(self, recipes_id: List[str]) -> Recipes: + # 레시피 리스트 조회 + recipes = self.recipes_repository.select_recipes_by_recipes_id(recipes_id) + return recipes -def get_ingredients_list_by_recipes(recipes: Recipes) -> List[Recipes]: - # 레시피 별로 재료 리스트 조회: - # [Ingredients(ingredients: [Ingredient, Ingredient]), Ingredients(ingredients: [Ingredient, Ingredient])] - ingredients_list = [select_ingredients_by_ingredients_id(recipe.get_ingredients()) for recipe in recipes.get_recipes()] - return ingredients_list + def get_ingredients_list_by_recipes(self, recipes: Recipes) -> List[Recipes]: + # 레시피 별로 재료 리스트 조회: + # [Ingredients(ingredients: [Ingredient, Ingredient]), Ingredients(ingredients: [Ingredient, Ingredient])] + ingredients_list = [ + self.recipes_repository.select_ingredients_by_ingredients_id(recipe.get_ingredients()) + for recipe in recipes.get_recipes() + ] + return ingredients_list -def update_cooked_recipes(user_id: str, recipes_id: str, feedback: bool): - user = get_user_by_user_id(user_id) - user_cooked_recipes_id = get_user_cooked_recipes_id_by_user(user) - user_recommended_recipes_id = get_user_recommended_recipes_id_by_user(user) - if feedback: - user_recommended_recipes_id.remove(recipes_id) - user_cooked_recipes_id.append(recipes_id) - return update_cooked_recipes(user_id, user_cooked_recipes_id, user_recommended_recipes_id) - user_cooked_recipes_id.remove(recipes_id) - user_recommended_recipes_id.append(recipes_id) - return update_cooked_recipes(user_id, user_cooked_recipes_id, user_recommended_recipes_id) - \ No newline at end of file + def update_cooked_recipes(self, user_id: str, recipes_id: str, feedback: bool): + user = self.get_user_by_user_id(user_id) + user_cooked_recipes_id = self.get_user_cooked_recipes_id_by_user(user) + user_recommended_recipes_id = self.get_user_recommended_recipes_id_by_user(user) + if feedback: + user_recommended_recipes_id.remove(recipes_id) + user_cooked_recipes_id.append(recipes_id) + return self.recipes_repository.update_cooked_recipes(user_id, user_cooked_recipes_id, user_recommended_recipes_id) + user_cooked_recipes_id.remove(recipes_id) + user_recommended_recipes_id.append(recipes_id) + return self.recipes_repository.update_cooked_recipes(user_id, user_cooked_recipes_id, user_recommended_recipes_id) + \ No newline at end of file From 6832d1c09deea5382718b28e479d0885b712c10c Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 01:10:08 +0900 Subject: [PATCH 071/187] =?UTF-8?q?test(test):=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EC=88=98=EC=A0=95=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- .../tests/api/routes/recipes/test_recipes.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/backend/tests/api/routes/recipes/test_recipes.py b/backend/tests/api/routes/recipes/test_recipes.py index d78c349..b52cc7b 100644 --- a/backend/tests/api/routes/recipes/test_recipes.py +++ b/backend/tests/api/routes/recipes/test_recipes.py @@ -1,5 +1,5 @@ import pytest -from .....app.api.routes.recipes.repository.recipes_repository import select_user_by_user_id, select_recipes_by_recipes_id, select_ingredients_by_ingredients_id +from .....app.api.routes.recipes.repository.recipes_repository import RecipesRepository from.....app.api.routes.recipes.entity.user import User from.....app.api.routes.recipes.entity.recipes import Recipes from.....app.api.routes.recipes.entity.recipe import Recipe @@ -9,13 +9,14 @@ from fastapi.testclient import TestClient from .....app.api.routes.recipes.controller.recipes_controller import recipes_router +recipes_repository = RecipesRepository() # User Type 확인 @pytest.mark.parametrize("user_id, output", [ ('65f0063d141b7b6fd385c7cc', True), ]) def test_select_user_by_user_id_type(user_id, output): - user = select_user_by_user_id(user_id) + user = recipes_repository.select_user_by_user_id(user_id) assert isinstance(user, User) == output @@ -51,7 +52,7 @@ def test_select_user_by_user_id_type(user_id, output): ), ]) def test_select_user_by_user_id(user_id, output): - user = select_user_by_user_id(user_id) + user = recipes_repository.select_user_by_user_id(user_id) assert user.model_dump() == output @@ -60,7 +61,7 @@ def test_select_user_by_user_id(user_id, output): (["65f0371e141b7b6fd385c7d8", "65f29506141b7b6fd385c7e9"], True), ]) def test_select_recipes_by_recipes_id_type(recipes_id, output): - recipes = select_recipes_by_recipes_id(recipes_id) + recipes = recipes_repository.select_recipes_by_recipes_id(recipes_id) assert isinstance(recipes, Recipes) == output assert isinstance(recipes.get_recipes()[0], Recipe) == output @@ -103,7 +104,7 @@ def test_select_recipes_by_recipes_id_type(recipes_id, output): ), ]) def test_select_recipes_by_recipes_id(recipes_id, output): - recipes = select_recipes_by_recipes_id(recipes_id) + recipes = recipes_repository.select_recipes_by_recipes_id(recipes_id) assert recipes.model_dump() == output @@ -113,7 +114,7 @@ def test_select_recipes_by_recipes_id(recipes_id, output): (["65f29547141b7b6fd385c7eb", "65f2955e141b7b6fd385c7f1"], True), ]) def test_select_ingredients_by_ingredients_id_type(ingredients_id, output): - ingredients = select_ingredients_by_ingredients_id(ingredients_id) + ingredients = recipes_repository.select_ingredients_by_ingredients_id(ingredients_id) assert isinstance(ingredients, Ingredients) == output assert isinstance(ingredients.get_ingredients()[0], Ingredient) == output @@ -140,13 +141,14 @@ def test_select_ingredients_by_ingredients_id_type(ingredients_id, output): ), ]) def test_select_ingredients_by_ingredients_id(ingredients_id, output): - ingredients = select_ingredients_by_ingredients_id(ingredients_id) + ingredients = recipes_repository.select_ingredients_by_ingredients_id(ingredients_id) assert ingredients.model_dump() == output # API 테스트 client = TestClient(recipes_router) +# 유저가 요리한 레시피 목록 조회 @pytest.mark.parametrize("user_id, output", [ ("65f0063d141b7b6fd385c7cc", { @@ -182,7 +184,7 @@ def test_get_user_cooked_recipes_by_page(user_id, output): assert response.status_code == 200 assert response.json() == output - +# 유저가 추천 받은 레시피 목록 조회 @pytest.mark.parametrize("user_id, output", [ ("65f0063d141b7b6fd385c7cc", { @@ -226,7 +228,15 @@ def test_get_user_cooked_recipes_by_page(user_id, output): ), ]) def test_get_user_recommended_recipes_by_page(user_id, output): - response = client.get(f"/users/{user_id}/recipes/recommended/") + response = client.get(f"/users/{user_id}/recipes/recommended") assert response.status_code == 200 assert response.json() == output + +# 유저가 요리한 레시피 수정 +@pytest.mark.parametrize("user_id, recipes_id, request_body, output", [ + ('65f0063d141b7b6fd385c7cc', '65f29506141b7b6fd385c7e9', {"feedback": "false"}, 200), +]) +def test_update_user_recipes_status__by_feedback(user_id, recipes_id, request_body, output): + response = client.patch(f"/users/{user_id}/recipes/{recipes_id}/feedback", json = request_body,) + assert response.status_code == output From 8a690e6659c04ea74ad742d836d1f48103e25d5d Mon Sep 17 00:00:00 2001 From: GangBean Date: Fri, 15 Mar 2024 13:58:19 +0900 Subject: [PATCH 072/187] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20API=20=EA=B5=AC=EC=A1=B0=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/request/signup_request.py | 4 +- .../users/controller/response/__init__.py | 0 .../users/controller/user_controller.py | 34 +++++-- backend/app/api/routes/users/dto/__init__.py | 0 backend/app/api/routes/users/entity/user.py | 5 +- .../users/repository/user_repository.py | 10 +- .../api/routes/users/service/user_service.py | 97 ++++++++++++++++++- 7 files changed, 134 insertions(+), 16 deletions(-) create mode 100644 backend/app/api/routes/users/controller/response/__init__.py create mode 100644 backend/app/api/routes/users/dto/__init__.py diff --git a/backend/app/api/routes/users/controller/request/signup_request.py b/backend/app/api/routes/users/controller/request/signup_request.py index bf3591d..eefd308 100644 --- a/backend/app/api/routes/users/controller/request/signup_request.py +++ b/backend/app/api/routes/users/controller/request/signup_request.py @@ -74,6 +74,6 @@ class UserFavorRecipesRequest(BaseModel): @validator('recipes') def validate_login_id(cls, recipes: list[str]): - if len(recipes) < MINIMUM_FAVOR_RECIPE_COUNT: - raise ValueError(f"좋아하는 레시피는 최소 {MINIMUM_FAVOR_RECIPE_COUNT} 개 이상이어야 합니다: {len(recipes)}") + if len(set(recipes)) < MINIMUM_FAVOR_RECIPE_COUNT: + raise ValueError(f"좋아하는 레시피는 최소 {MINIMUM_FAVOR_RECIPE_COUNT} 개 이상이어야 합니다: {len(set(recipes))}") return recipes diff --git a/backend/app/api/routes/users/controller/response/__init__.py b/backend/app/api/routes/users/controller/response/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index d211a3a..59421b1 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -9,7 +9,7 @@ from .request.signup_request import SignupRequest, LoginRequest, UserFavorRecipesRequest from .response.signup_response import SignupResponse, LoginResponse, FavorRecipesResponse from ..service.user_service import UserService -from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository +from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository, RecommendationRepository from ..dto.user_dto import UserSignupDTO, UserLoginDTO class UserController: @@ -25,24 +25,37 @@ async def sign_up(self, signup_request: SignupRequest) -> SignupResponse: async def login(self, login_request: UserLoginDTO) -> LoginResponse: return LoginResponse( - **dict(self.service.login(login_request)) + **dict(self.user_service.login(login_request)) ) async def is_login_id_usable(self, login_id: str) -> bool: - return self.service.is_login_id_usable(login_id) + return self.user_service.is_login_id_usable(login_id) async def is_nickname_usable(self, nickname: str) -> bool: - return self.service.is_nickname_usable(nickname) + return self.user_service.is_nickname_usable(nickname) async def favor_recipes(self, page_num: int) -> list: - return self.service.favor_recipes(page_num) + return self.user_service.favor_recipes(page_num) async def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: - return self.service.save_favor_recipes(login_id, request) + return self.user_service.save_favor_recipes(login_id, request) + + async def recommended_basket(self, user_id: str, price: int): + # top k recipes id 가져옴 + top_k_recipes = self.user_service.top_k_recipes(user_id, price) + + # recipe 정보 가져오기 + recipe_infos = {} # self.recipe_service.infos(top_k_recipes) + + # ingredient 정보 가져오기 + ingredient_infos = {} # self.ingredient_service.infos(recipe_infos) + + basket_info = self.user_service.recommended_basket(recipe_infos) + return user_controller = UserController(UserService( - UserRepository(), SessionRepository(), FoodRepository())) + UserRepository(), SessionRepository(), FoodRepository(), RecommendationRepository())) user_router = APIRouter() class Request(BaseModel): @@ -59,7 +72,7 @@ async def sign_up(request: SignupRequest) -> JSONResponse: password=request.password, nickname=request.nickname, email=request.email)) -x return JSONResponse(content=response_body.model_dump(), status_code=status.HTTP_200_OK) + return JSONResponse(content=response_body.model_dump(), status_code=status.HTTP_200_OK) @user_router.post('/api/users/auth') async def login(request: LoginRequest) -> Response: @@ -94,3 +107,8 @@ async def favor_recipes(page_num: int=1) -> Response: async def save_favor_recipes(user_id: str, request: UserFavorRecipesRequest) -> Response: await user_controller.save_favor_recipes(user_id, request) return Response(status_code=status.HTTP_200_OK) + +@user_router.post('/api/users/{user_id}/recommendations') +async def get_recommendation(user_id: str, price: int) -> JSONResponse: + response_body = {}# await user_controller.recommended_basket(user_id, price) + return JSONResponse(content=response_body, status_code=status.HTTP_200_OK) diff --git a/backend/app/api/routes/users/dto/__init__.py b/backend/app/api/routes/users/dto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/users/entity/user.py b/backend/app/api/routes/users/entity/user.py index f8c94f7..03b8b16 100644 --- a/backend/app/api/routes/users/entity/user.py +++ b/backend/app/api/routes/users/entity/user.py @@ -10,4 +10,7 @@ class User(BaseModel): class Session(BaseModel): _id: Optional[str] - user_id: str \ No newline at end of file + user_id: str + +class Recommendation: + recipe_id: str diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index 58d73c9..c147139 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -57,4 +57,12 @@ def find_foods(self, page_num: int, page_size: int=10) -> list: for result in results: result['_id'] = str(result['_id']) lst.append(result) - return lst \ No newline at end of file + return lst + +class RecommendationRepository: + def __init__(self): + self.collection = data_source.collection_with_name_as('model_recommendation_histories') + + def find_by_login_id(self, login_id: str) -> list: + result = self.collection.find_one({'login_id': login_id}) + return result['recipe_top_20'] diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index ce71e0f..313db1a 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -1,29 +1,38 @@ import uuid +import pulp + from datetime import datetime, timedelta from ..entity.user import User -from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository +from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository, RecommendationRepository from ..dto.user_dto import ( UserSignupDTO, UserLoginDTO, ) from ..controller.request.signup_request import UserFavorRecipesRequest class UserService: - def __init__(self, user_repository :UserRepository, session_repository: SessionRepository, food_repository: FoodRepository): + def __init__(self, + user_repository :UserRepository, + session_repository: SessionRepository, + food_repository: FoodRepository, + recommendation_repository: RecommendationRepository, + ): self.user_repository: UserRepository = user_repository self.session_repository: SessionRepository = session_repository self.food_repository: FoodRepository = food_repository + self.recommendation_repository : RecommendationRepository = recommendation_repository def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: return self.user_repository.insert_one(sign_up_request) def login(self, login_request: UserLoginDTO) -> UserLoginDTO: - user = User(**dict(self.user_repository.find_one({'login_id': login_request.login_id, 'password': login_request.password}))) + user = self.user_repository.find_one({'login_id': login_request.login_id, 'password': login_request.password}) if user is None: raise ValueError("아이디와 비밀번호가 일치하지 않습니다.") + user = User(**dict(user)) token = str(uuid.uuid4()) - expire_date = datetime.utcnow() + timedelta(seconds=30 * 60) + expire_date = datetime.now() + timedelta(seconds=30 * 60) return self.session_repository.insert_one(login_id=login_request.login_id, token=token, expire_date=expire_date) def is_login_id_usable(self, login_id: str) -> bool: @@ -41,3 +50,83 @@ def favor_recipes(self, page_num: int) -> list: def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: self.user_repository.update_food(login_id, request.recipes) + + def top_k_recipes(self, login_id: str, price: int) -> list: + # user에 inference된 recipes + return self.recommendation_repository.find({'login_id': login_id}) + + def recommended_basket(self, recipe_infos: dict, price: int) -> dict: + # 입력값 파싱 + recipe_requirement_infos, price_infos = self._parse(recipe_infos) + + # 이진 정수 프로그래밍 + return self._optimized_results(recipe_requirement_infos, price_infos, price) + + def _parse(self, recipe_infos: dict): + recipe_requirement_infos, price_infos = None, None + return recipe_requirement_infos, price_infos + + def _optimized_results(self, + recipe_requirement_infos: dict, + price_infos: dict, + price: int): + # 혼합 정수 프로그래밍 + # 가격 상한 + MAX_PRICE = price + + # 문제 인스턴스 생성 + prob = pulp.LpProblem("MaximizeNumberOfDishes", pulp.LpMaximize) + + # ingredients_info = { + # 'A': {'price': 70, 'amount': 5}, + # 'B': {'price': 30, 'amount': 2}, + # 'C': {'price': 20, 'amount': 2}} + # ingredients_price = {'신김치': 7000, '돼지고기': 5000, '양파': 3000, '두부': 1500, '애호박': 2000, '청양고추': 1000, '계란': 6000, '밀가루': 3000} + # 식재료 가격 및 판매단위 변수 (0 또는 1의 값을 가짐) + x = pulp.LpVariable.dicts("ingredient", price_infos.keys(), cat='Binary') # 재료 포함 여부 + + # 요리 변수 + # dishes = ['Dish1', 'Dish2'] + dishes = recipe_requirement_infos.keys() + # dishes = ['돼지고기김치찌개', '된장찌개', '애호박전'] + y = pulp.LpVariable.dicts("dish", dishes, cat='Binary') # 요리 포함 여부 + + # 요리별 필요한 식재료 양 + # requirements = {'Dish1': {'A': 5, 'B': 2}, 'Dish2': {'B': 2, 'C': 2}} + # requirements = {'돼지고기김치찌개': ['신김치', '돼지고기', '양파', '두부'], '된장찌개': ['두부', '애호박', '청양고추', '양파'], '애호박전': ['애호박', '계란', '밀가루']} + + # 식재료 제한: 각 요리를 최대한 살 수 있는 한도 + a = {ingredient: (MAX_PRICE//info['price'])*info['amount'] \ + for ingredient, info in price_infos.items()} + print(a) + + # 목표 함수 (최대화하고자 하는 요리의 수) + prob += pulp.lpSum([y[dish] for dish in dishes]) + + # 비용 제약 조건 + prob += pulp.lpSum([price_infos[i]['price']*x[i] for i in price_infos]) <= MAX_PRICE + + # 요리별 식재료 제약 조건 + for dish in dishes: + for ingredient in recipe_requirement_infos[dish].keys(): + prob += (x[ingredient] >= y[dish]) + + # 정량 제약 조건: 요리에 사용되는 재료의 총합은 각 재료의 상한을 넘지 못함 + for ingredient in price_infos.keys(): + total_amount = pulp.lpSum([y[dish] * requirement[ingredient] for dish, requirement in recipe_requirement_infos.items() if ingredient in requirement]) + prob += (total_amount <= a[ingredient] * x[ingredient]) + + # 문제 풀기 + prob.solve() + + # 결과 출력 + print("Status:", pulp.LpStatus[prob.status]) + + for dish in dishes: + print(f"Make {dish}:", y[dish].varValue) + for ingredient in price_infos: + print(f"Use Ingredient {ingredient}:", x[ingredient].varValue) + result_dish = [dish for dish in dishes if y[dish].varValue == 1] + result_ingredient = [ingredient for ingredient in price_infos if x[ingredient].varValue == 1] + + return {'recipe_list' : result_dish, 'ingredient_list': result_ingredient} From d3d155e422a164be216f3505c3ca315219fa4c17 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Fri, 15 Mar 2024 14:43:16 +0900 Subject: [PATCH 073/187] =?UTF-8?q?fix(service):=20get=5Fingredients=5Flis?= =?UTF-8?q?t=5Fby=5Frecipes=20=EB=A9=94=EC=86=8C=EB=93=9C=EC=9D=98=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20=EA=B0=92=20type=20hint=EB=A5=BC=20List[In?= =?UTF-8?q?gredients]=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 22 --- backend/app/api/routes/recipes/service/recipes_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py index 269b228..56936e9 100644 --- a/backend/app/api/routes/recipes/service/recipes_service.py +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -1,6 +1,7 @@ from typing import List from ..repository.recipes_repository import RecipesRepository from ..entity.recipes import Recipes +from ..entity.ingredients import Ingredients from ..entity.user import User @@ -30,7 +31,7 @@ def get_recipes_by_recipes_id(self, recipes_id: List[str]) -> Recipes: return recipes - def get_ingredients_list_by_recipes(self, recipes: Recipes) -> List[Recipes]: + def get_ingredients_list_by_recipes(self, recipes: Recipes) -> List[Ingredients]: # 레시피 별로 재료 리스트 조회: # [Ingredients(ingredients: [Ingredient, Ingredient]), Ingredients(ingredients: [Ingredient, Ingredient])] ingredients_list = [ From a24a258f5ae4568fafeb73acb5a0a116e98e3819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:48:10 +0900 Subject: [PATCH 074/187] Delete .gitignore --- .gitignore | 210 ----------------------------------------------------- 1 file changed, 210 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 330f0e6..0000000 --- a/.gitignore +++ /dev/null @@ -1,210 +0,0 @@ -# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,linux -# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,linux - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### Python ### -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -### Python Patch ### -# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration -poetry.toml - -# ruff -.ruff_cache/ - -# LSP config files -pyrightconfig.json - -### VisualStudioCode ### -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ - -# Built Visual Studio Code Extensions -*.vsix - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history -.ionide - -# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,linux \ No newline at end of file From 8e87c2c40b2d38126ad461c0b5045916bb597059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=84=B1=ED=99=8D=5FT6165?= <35794681+GangBean@users.noreply.github.com> Date: Fri, 15 Mar 2024 14:48:22 +0900 Subject: [PATCH 075/187] Delete README.xxx --- README.xxx | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 README.xxx diff --git a/README.xxx b/README.xxx deleted file mode 100644 index e282165..0000000 --- a/README.xxx +++ /dev/null @@ -1,4 +0,0 @@ -./configure -make -make test -sudo make install From be5bd6fcec9350ad5e88c1bf57ba013cd32febfc Mon Sep 17 00:00:00 2001 From: GangBean Date: Fri, 15 Mar 2024 15:42:47 +0900 Subject: [PATCH 076/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20API=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20datasourc?= =?UTF-8?q?e=20ip=20=EB=B3=80=EA=B2=BD=20#32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/routes/users/controller/user_controller.py | 2 +- backend/app/database/data_source.py | 2 +- backend/app/database/dev.env | 2 +- backend/app/database/prod.env | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 59421b1..0d9ec95 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -14,7 +14,7 @@ class UserController: def __init__(self, user_service: UserService): - self.service: UserService = user_service + self.user_service: UserService = user_service async def sign_up(self, signup_request: SignupRequest) -> SignupResponse: return SignupResponse( diff --git a/backend/app/database/data_source.py b/backend/app/database/data_source.py index 43163fa..c6808e1 100644 --- a/backend/app/database/data_source.py +++ b/backend/app/database/data_source.py @@ -7,7 +7,7 @@ from pydantic import BaseModel from typing import Optional -from ..exception.database.database_exception import ( +from exception.database.database_exception import ( DatabaseNotFoundException, CollectionNotFoundException ) diff --git a/backend/app/database/dev.env b/backend/app/database/dev.env index 21fabab..00b7026 100644 --- a/backend/app/database/dev.env +++ b/backend/app/database/dev.env @@ -1,4 +1,4 @@ ENV=dev -HOST=localhost +HOST=10.0.7.6 PORT=27017 DATABASE=dev diff --git a/backend/app/database/prod.env b/backend/app/database/prod.env index 7f55659..8f4c281 100644 --- a/backend/app/database/prod.env +++ b/backend/app/database/prod.env @@ -1,4 +1,4 @@ ENV=prod -HOST=10.0.6.6 +HOST=10.0.7.6 PORT=27017 DATABASE=prod From 90ddffa2b6de340e67cd90b19ff5a00c34fefa82 Mon Sep 17 00:00:00 2001 From: GangBean Date: Fri, 15 Mar 2024 15:48:32 +0900 Subject: [PATCH 077/187] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20API=20=EC=88=98=EC=A0=95=20#32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/routes/users/controller/user_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 0d9ec95..8931473 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -18,7 +18,7 @@ def __init__(self, user_service: UserService): async def sign_up(self, signup_request: SignupRequest) -> SignupResponse: return SignupResponse( - **dict(self.service.sign_up( + **dict(self.user_service.sign_up( UserSignupDTO(**dict(signup_request)) )) ) From ef6118a38c0074b25c59ba4e350cd8c351a23048 Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Fri, 15 Mar 2024 17:35:57 +0900 Subject: [PATCH 078/187] feat: Update app.py --- frontend/Home.py | 26 - frontend/app.py | 557 ++++++++++++++++++++ frontend/img/food.png | Bin 0 -> 4313 bytes frontend/img/howto.png | Bin 0 -> 7060 bytes frontend/img/logo.png | Bin 0 -> 482 bytes frontend/img/potato.png | Bin 0 -> 1531 bytes frontend/pages/RecommendationHistoryPage.py | 89 ---- frontend/pages/RecommendationPage.py | 51 -- frontend/pages/ResultPage2.py | 69 --- frontend/pages/UserHistoryPage.py | 43 -- frontend/requirements.txt | 5 + frontend/utils.py | 74 --- 12 files changed, 562 insertions(+), 352 deletions(-) delete mode 100644 frontend/Home.py create mode 100644 frontend/app.py create mode 100644 frontend/img/food.png create mode 100644 frontend/img/howto.png create mode 100644 frontend/img/logo.png create mode 100644 frontend/img/potato.png delete mode 100644 frontend/pages/RecommendationHistoryPage.py delete mode 100644 frontend/pages/RecommendationPage.py delete mode 100644 frontend/pages/ResultPage2.py delete mode 100644 frontend/pages/UserHistoryPage.py create mode 100644 frontend/requirements.txt diff --git a/frontend/Home.py b/frontend/Home.py deleted file mode 100644 index d03da30..0000000 --- a/frontend/Home.py +++ /dev/null @@ -1,26 +0,0 @@ -# Temporal Entrypoint -import streamlit as st - -from utils import page_header - -# define session-page -def home(): - page_header() - -# 세션 초기화 -if 'page_info' not in st.session_state: - st.session_state['page_info'] = 'home' - -# 로그인되어 있다고 가정 -def init(): - st.session_state.user = 1 - st.session_state.is_authenticated = True - -# 로그인 상태 초기화 -init() - - -# page_info 설정 -st.session_state['page_info'] = 'home' - -home() diff --git a/frontend/app.py b/frontend/app.py new file mode 100644 index 0000000..b4b7950 --- /dev/null +++ b/frontend/app.py @@ -0,0 +1,557 @@ +import streamlit as st +import streamlit_antd_components as sac +from streamlit_extras.stylable_container import stylable_container +from st_supabase_connection import SupabaseConnection +from st_login_form import login_form +from streamlit_login_auth_ui.widgets import __login__ +import time, math + +import streamlit as st +import pandas as pd +import numpy as np + +from PIL import Image +import requests + +global api_prefix +api_prefix = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/" + +def display_ingredients_in_rows_of_four2(ingredients): + for ingredient in ingredients: + sub_container = st.container(border=True) + + with sub_container: + + cols = st.columns(5) + + with cols[0]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + with cols[1]: + st.write(ingredient['ingredient_name']) + st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) + + with cols[-1]: + st.link_button('구매', ingredient['market_url'], type='primary') + +def show_feedback_button(recipe_id, user_feedback): + + icon_mapper = lambda cooked: '❤️' if cooked else '🩶' + cooked = recipe_id in user_feedback + + st.button( + icon_mapper(cooked), + on_click=patch_feedback, + key=f'{recipe_id}_feedback_button', + args=(st.session_state.user, recipe_id, cooked)) + +def display_recipes_in_rows_of_four(recipe_list, user_feedback=None): + + for row in range(math.ceil(len(recipe_list)/4)): + cols = st.columns(4) + + for i in range(4): + item_idx = i + row * 4 + if item_idx >= len(recipe_list): break + + item = recipe_list[item_idx] + with cols[i]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + if user_feedback is None: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + else: + sub_cols = st.columns([3,1]) + with sub_cols[0]: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + with sub_cols[-1]: + show_feedback_button(item['recipe_id'], user_feedback) + + +def get_and_stack_recipe_data_w_feedback(): + + url = api_prefix + "users/{user_id}/recipes/recommended?page={page_num}" + recipe_list, user_feedback = [], [] + formatted_url = url.format(user_id=st.session_state.user, page_num=1) + + while formatted_url: + print(formatted_url) + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + print(data['user_feedback']) + user_feedback = data['user_feedback'] + + return recipe_list, user_feedback + +def recommendation_history_page(): + + # 앱 헤더 + page_header() + + # get data + recipe_list, user_feedback = get_and_stack_recipe_data() + + # 페이지 구성 + container = st.container(border=True) + + with container: + + st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) + + sub_container = st.container(border=False) + with sub_container: + st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) + st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) + + display_ingredients_in_rows_of_four2(recipe_list, user_feedback) + + +def recommendation_page(): + + # 앱 헤더 + page_header() + + # 페이지 구성 + container = st.container(border=True) + + with container: + st.markdown("

이번 주 장바구니 만들기

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + st.markdown("
예산을 정해주세요.
", unsafe_allow_html=True) + + cols = st.columns([1,5,1]) + + with cols[1]: + + price = st.slider( + label='', min_value=10000, max_value=1000000, value=50000, step=5000 + ) + + cols = st.columns(5) + + with cols[2]: + st.write("예산: ", price, '원') + + + cols = st.columns(3) + + with cols[1]: + button2 = st.button("장바구니 추천받기", type="primary") + if button2: + st.session_state['page_info'] = 'result_page_1' + + + +def display_ingredients_in_rows_of_four(ingredients): + for ingredient in ingredients: + sub_container = st.container(border=True) + + with sub_container: + + cols = st.columns(5) + + with cols[0]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + with cols[1]: + st.write(ingredient['ingredient_name']) + st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) + + with cols[-1]: + st.link_button('구매', ingredient['market_url'], type='primary') +def display_recipes_in_rows_of_four(recipes): + for recipe in recipes: + sub_container = st.container(border=True) + + with sub_container: + + cols = st.columns(2) + + with cols[0]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + with cols[1]: + st.write(recipe['recipe_name']) + + + +def result_page_2(): + + # 앱 헤더 + page_header() + + url = api_prefix + "users/{user_id}/previousrecommendation" + formatted_url = url.format(user_id=st.session_state.user) + data = get_response(formatted_url) + + # 페이지 구성 + container = st.container(border=True) + + with container: + + # 장바구니 추천 문구 + st.markdown("

새로운 장바구니를 추천받았어요!

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + + st.divider() + + # 구매할 식료품 목록 + st.markdown("

추천 장바구니

", unsafe_allow_html=True) + + display_ingredients_in_rows_of_four(data['ingredient_list']) + total_price = sum([ingredient['ingredient_price'] for ingredient in data['ingredient_list']]) + + st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) + + st.divider() + + # 이 장바구니로 만들 수 있는 음식 레시피 + st.markdown("

이 장바구니로 만들 수 있는 음식 레시피

", unsafe_allow_html=True) + display_recipes_in_rows_of_four(data['recipe_list']) + + st.text("\n\n") + basket_feedback() + + +def get_and_stack_recipe_data(): + + url = api_prefix + "users/{user_id}/recipes/cooked?page={page_num}" + recipe_list = [] + formatted_url = url.format(user_id=st.session_state.user, page_num=1) + + while formatted_url: + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + + return recipe_list + + + +def get_my_recipe_data(): + + url = api_prefix + "users/{user_id}/recipes/recommended?page={page_num}" + recipe_list = [] + formatted_url = url.format(user_id=st.session_state.user, page_num=1) + + while formatted_url: + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + + return recipe_list + + + +def user_history_page(): + + # 앱 헤더 + page_header() + + # get data + recipe_list = get_and_stack_recipe_data() + + # show container + container = st.container(border=True) + + with container: + # title + st.markdown("

❤️ 내가 요리한 레시피 ❤️

", unsafe_allow_html=True) + display_ingredients_in_rows_of_four(recipe_list) + +# , signin_page, login_page, signin_page_2, signin_page_3, main_page_2 +menu_titles = ["house", "🛒이번주 장바구니 추천", "😋내가 요리한 레시피", "🔎취향저격 레시피", "Log In / 회원가입"] + +def main_page(): + container3_1 = stylable_container( + key="container_with_border", + css_styles=""" + { + border: 1px solid rgba(49, 51, 63, 0.2); + border-radius: 0.5rem; + padding: calc(1em - 1px); + } + """,) + with container3_1: + st.markdown("

나만의 식량 바구니에 \n 오신 것을 환영합니다!

", unsafe_allow_html=True) + st.markdown("

자신의 입맞에 맞는 레시피를 저장하고 \n 이번주에 구매할 식량 바구니를 추천받아보세요

", unsafe_allow_html=True) + + btn = sac.buttons( + items=['로그인', '회원가입'], + index=0, + format_func='title', + align='center', + direction='horizontal', + radius='lg', + return_index=False + ) + + container3_2 = st.container(border = True) + with container3_2: + st.markdown("

사용 방법

", unsafe_allow_html=True) + st.markdown("

회원 가입을 했을 때 어떤 기능을 쓸 수 있는지 살펴보는 페이지

", unsafe_allow_html=True) + left_co, cent_co,last_co = st.columns((1, 8, 1)) + with cent_co: + st.image('img/howto.png') + +def signin_page(): + container3 = st.container(border = True) + with container3: + st.markdown(f"

{menu_titles[4]}

", unsafe_allow_html=True) + + __login__obj = __login__( + auth_token = "courier_auth_token", + company_name = "Shims", + width = 200, height = 250, + logout_button_name = 'Logout', + hide_menu_bool = False, + hide_footer_bool = False) + + LOGGED_IN = __login__obj.build_login_ui() + + if LOGGED_IN == True: + st.markown("Your Streamlit Application Begins here!") + +def user_history_page(): + + recipe_list = get_and_stack_recipe_data() + + container = st.container(border=True) + + my_recipe_list = get_my_recipe_data() + + container3 = st.container(border=True) + with container3: + st.markdown("

최근에 만들었던 음식을 골라주세요

", unsafe_allow_html=True) + st.markdown("

5개 이상 선택해 주세요

", unsafe_allow_html=True) + cols2 = st.columns([4,4]) + + + col1, col2, col3, col4 = st.columns(4) + with col1: + with st.container(border=True): + my_recipe = my_recipe_list[0] + st.image(my_recipe["recipe_img_url"]) + st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) + with col2: + with st.container(border=True): + my_recipe = my_recipe_list[1] + st.image(my_recipe["recipe_img_url"]) + st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) + with col3: + with st.container(border=True): + my_recipe = my_recipe_list[2] + st.image(my_recipe["recipe_img_url"]) + st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) + with col4: + with st.container(border=True): + my_recipe = my_recipe_list[3] + st.image(my_recipe["recipe_img_url"]) + st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) + + btn = sac.buttons( + items=['더 보기', '다음 단계'], + index=0, + format_func='title', + align='center', + direction='horizontal', + radius='lg', + return_index=False, + ) + +def main_page_2(): + container3_1 = stylable_container( + key="container_with_border", + css_styles=""" + { + border: 1px solid rgba(49, 51, 63, 0.2); + border-radius: 0.5rem; + padding: calc(1em - 1px); + background-color: white; + } + """,) + with container3_1: + st.markdown("

나만의 식량 바구니에 \n 오신 것을 환영합니다!

", unsafe_allow_html=True) + st.markdown("

자신의 입맞에 맞는 레시피를 저장하고 \n 이번주에 구매할 식량 바구니를 추천받아보세요

", unsafe_allow_html=True) + + my_recipe_list = get_my_recipe_data() + # 취향저격 레시피 + container3_4 = st.container(border = True) + with container3_4: + st.markdown("

내가 해먹은 레시피

", unsafe_allow_html=True) + + col_1, col_2, col_3, col_4, col_5 = st.columns((1, 1, 1, 1, 1)) + + with col_1: + with st.container(border=True): + st.image(my_recipe_list[0]["recipe_img_url"]) + st.markdown(f"

{my_recipe_list[0]['recipe_name']}

", unsafe_allow_html=True) + with col_2: + with st.container(border=True): + st.image(my_recipe_list[1]["recipe_img_url"]) + st.markdown(f"

{my_recipe_list[1]['recipe_name']}

", unsafe_allow_html=True) + with col_3: + with st.container(border=True): + st.image(my_recipe_list[2]["recipe_img_url"]) + st.markdown(f"

{my_recipe_list[2]['recipe_name']}

", unsafe_allow_html=True) + with col_4: + with st.container(border=True): + #col_l, col_c, + st.image(my_recipe_list[3]["recipe_img_url"]) + st.markdown(f"

{my_recipe_list[3]['recipe_name']}

", unsafe_allow_html=True) + with col_5: + with st.container(border=True): + st.image(my_recipe_list[4]["recipe_img_url"]) + st.markdown(f"

{my_recipe_list[4]['recipe_name']}

", unsafe_allow_html=True) + + +def set_logout(): + st.session_state.user = None + st.session_state.is_authenticated = False + +def set_login(): + st.session_state.user = 'judy123' + st.session_state.is_authenticated = True + +def login_button(): + if st.session_state.is_authenticated: + login_button = st.button(f"{st.session_state.user}님 | 로그아웃", on_click=set_logout) + else: + login_button = st.button(f"회원가입 | 로그인", on_click=set_login) + + return login_button + +def page_header(): + cols = st.columns([8,3]) + with cols[0]: + st.header('나만의 식량 바구니') + with cols[-1]: + login_button() + button_css() + +def basket_feedback(): + st.markdown("
방금 추천받은 장바구니 어땠나요?
", unsafe_allow_html=True) + st.text("") + cols = st.columns([3,1,1,3]) + with cols[1]: + st.button('좋아요') + with cols[2]: + st.button('싫어요') + +def get_response(formatted_url): + response = requests.get(formatted_url) + if response.status_code == 200: + data = response.json() + else: + print(f'status code: {response.status_code}') + data = None + return data + +def patch_feedback(user_id, recipe_id, current_state): + url = api_prefix + "users/{user_id}/recipes/{recipe_id}/feedback" + data = { + 'feedback': not current_state + } + response = requests.patch(url.format(user_id=user_id, recipe_id=recipe_id), json=data) + print(f'status code: {response.status_code}') + st.rerun() + +def button_css(): + st.markdown( + """""", + unsafe_allow_html=True, + ) + + +st.set_page_config(layout="wide") + +# 세션 초기화 +if 'page_info' not in st.session_state: + st.session_state['page_info'] = 'home' + +# 로그인되어 있다고 가정 +def init(): + st.session_state.user = 1 + st.session_state.is_authenticated = True + +# 로그인 상태 초기화 +init() + + +# page_info 설정 +st.session_state['page_info'] = 'home' + + +app_title = "🛒 나만의 식량 바구니" +menu_titles = ["house", "🛒이번주 장바구니 추천", "😋내가 요리한 레시피", "🔎취향저격 레시피", "Log In / 회원가입", "MainPage-2", "User history"] +################ + +################ +# body -> main -> sub +container1 = st.container(border=True) +with container1: + cols = st.columns([1,2]) + with cols[0]: + st.markdown(f"

{app_title}

", unsafe_allow_html=True) + with cols[1]: + seg = sac.segmented( + items=[ + sac.SegmentedItem(icon=menu_titles[0]), + sac.SegmentedItem(label=menu_titles[1]), + sac.SegmentedItem(label=menu_titles[2]), + sac.SegmentedItem(label=menu_titles[3]), + sac.SegmentedItem(label=menu_titles[4]), + sac.SegmentedItem(label=menu_titles[5]), + sac.SegmentedItem(label=menu_titles[6]), + ], align='center', use_container_width=True, + ) + + container2 = st.container(border=True) + with container2: + if seg == menu_titles[1]: + # 🛒이번주 장바구니 추천 + container3 = st.container(border=True) + with container3: + st.markdown(f"

{menu_titles[1]}

", unsafe_allow_html=True) + + if 'page_info' not in st.session_state: + st.session_state['page_info'] = 'recommend' + + if st.session_state['page_info'] == 'result_page_1': + result_page_2() + else: + recommendation_page() + + elif seg == menu_titles[2]: + # 😋내가 요리한 레시피 + st.session_state['page_info'] = 'recommend_history' + main_page_2() + elif seg == menu_titles[3]: + st.session_state['page_info'] = 'recommend_history' + main_page_2() + elif seg == menu_titles[4]: + # 로그인 / 회원가입 + signin_page() + elif seg == menu_titles[5]: + # MainPage_2 + # main_page_2() + + st.session_state['page_info'] = "result_page_2" + result_page_2() + elif seg == menu_titles[6]: + + # show UserHistoryPage + st.session_state['page_info'] = 'user_history' + user_history_page() + else : + # Home + main_page() + \ No newline at end of file diff --git a/frontend/img/food.png b/frontend/img/food.png new file mode 100644 index 0000000000000000000000000000000000000000..845bf8a446b955948e5badd4babfe79634b359b2 GIT binary patch literal 4313 zcmb`L=QkSw8^w*JMp3Kw4zZ)QR;>s^?NQX;gqpQ#6MNLEtwo7ld&OQwQKhs-ZLwEt zRYSae|AzN{&biM$zYq8GJfl!vM>pd?IsN zr^UY2T;Zm|mHhA$lX_Tu!owkIl@HTJwzEGOMWz{^*&b_mZ%H7M#(1uiG{{0fnb){b z>&PX5ne>#0to!f5X-Y>S^@okVjy+C(KVk;H;N^{noZU^nSuXrHS9kI!XYwa_^Q{Mx zaj~rJn`yDYE+LB3Mw!0x4Lp%V!v8PyE?7-}Z4akRq}mQxAf;6?)pR7APWjQxsspE! zpdIuy=jI|Npdlvd2(PGlQt*zy4_J#OFgX4Q5#9VGP4Y`^Uo78chCm<}qy?tos+G`c z+Kp^IZMR>Ca#C;spz*MQfD(Aqm~(sP?OcR}fy;X3gyD$S5z1pWTw4@$Fj?AJ9lV&S z9+L{MG^vr!s{wzbG9Mv7;e_rYxPrOn+k~lc=c6nmjzHZNL58A zz{`CWxdW%yQT~wCX!1nEbjd-fc@s*-nGs)xgac5z-;x^(qO9(w65bLUbMChu@vTo( zQsbk1e2pQ&JOiuP^qDtEDXxU;h@@8K0)h-meB=FqIhKe`Do!iA+Q0La{FRl)Z6`ud z_?6qnp?YF=Wy$O6>V4;#eVHoOW<5GEb!SJrgkdI|22Mseu$n6O2s>PoiW%p+=@tvR z+?REGB?)ai#4>71Db>r#D;6+3A$~`-8YaFkA~=GE%4$nd~uVAvy(*NoD;_W=Y?5xP&Idz#>w6p3aQirQ08Q9`Gta=%K8 zA?3bKt<-haI+jre|Gb2C(8%~1gk{0&BFhw}NaxeF1eW&P*bFYA&K9OrB364e%dU2RKMq8vUb#b(s_3}7#-P&oSh9@SaBq# zIBxx&9CKZ_pQbVn@DN{X9x9Fdm@J?-H?tY|TC3JBEPoCBY;W)4OCQAezJ#e-(9jTu zD>0=*KB%op38%Z(H<}urP~O<&Ub}2!rbknVbxnCWv@$rt>v@2RV3y}o2)Z;ksc37< zUcVT|r=I4?gHi|12C?s@+~QeqBPz~yJ9eH9WRqbWV=oi=ap<(~KO3E9qy~y3v(f0C zyi2G)01RPgFbk{= zuzdq5*S@7hK~(0Xb81n=JYc}+S8T5@-M|QY z>DouvTxd0sHo?F>P;B5A4W`SDq|F@tO=4BDhgN&yKF8N`cba5Jt?YX*DD>+IpWC3F zvj$Gol783yRejZyyc?_EPTk#6bAn~Z9D5WURI~L8nR82JKJ1YyQFzIijAYcjdQxZv zh~tWS7^)FvX*U}{$ z#wWJ*F%0X<{;0)cHDzF?vEZa98W^*=EY+_;r;-+dkE4fJwaketq(2wWNBW*Q4x4#w z1purZ{%-5{8^Ffb=O5XEx7jCw1ioDquq+_pZJ=yR`M zr}$faW5nie?L|)tz0N&d{5;rD_IsVd`ay~0>GPRCf3mGBL_&Q(XS0VWj+PRnnFzgI zS>o2`Yhq>hzgJxV99JxcF_l2#!vshztH!&QqYwr55S|y^0?laE&I@r#@$YOjmyXcZ z$n)5z%|>ZN6w^Ld0^wA+OS7q)5MM9?J!xLb5`reN`i&al^qIVF&HzUHL&;FIc`Rb? zU0*and`kuWt*`K`Y&#;IYo6`1Rd|zFG?h5^k1MNGNo?zLC@cIW2ihTYN9mJ84=`~4Ns(f~pW1sdg^Cf!rNpW}AxdGS+fEx%~9KQ5$e z>Fk4-uFJG!3yOl~9DB);=KEXIYj~(JJ#2}(v##!az}P_Q7|mzU>%rJ(Y*W-oD)Qo7_F} z3Pu+3EZs7hl{P{I$AfUba*|lZY!TH97SA-4g`Qov9WRG+Zf>IL8?607w0^Pq^xu~x z?u{Hifqw()m>>5oTf91FFw~)(x|HSpc+c0`rvIU!dIO(8fui2%@?fpO4{dnr!91|f zIjXj$wK;*Z()H<6*}ZBF+mPUtpMsy)ydUBi{0=^B?qv3_mzI@{+OIK~hQo!bA%Ql! z@0WO}bv!n^qZzj7=NIcfQ@}_II!sqwct5Z8ad>Ssm*ak|VUy|?i>__yY&tue@hHvpDzwTVJta37+2R}n`{1VJzdF5f}j%)~r1ci9r36Gf) z`}!VwS(87Iw*e1N6sA7^S7aJ;3Fk)FV2_d9AQ2t1cK!Uk$b)XFuw5~CHx>!<)|~!j zgezLqRkNJot83k;)~AHV>W7mzP}G3Q&GV=aLS&#Vjk4GxgPWZ9O%|-OkMXkskG1`r zqITTA6Uvm-^!PZAKohxM?+9Ou3kVCVFC@vY()nWYR-|jl;w=iLqhSQscLN_R>s+%S z9C5;QaZbFRqoxGV66*WnQr%Z`*+FU3HAdOfx)LFFEJ)y3&kyKVsB?9EL*@REwHgBLD?8HLSgplWZ>oY7r_8DXEW>Rk_EP z;t%)w8&kPQtJ4CTqc8>RZfgM6iTJFYRfw=!qf4i zOTpn$boI-rCBfK_reWSXF3KMk1}27t2N4VjnUFc&Z}h{e{wE8CC@UNee!HFNiM;8V zzq~J=Spem@ID*&|5+xU&Ue65MIinwtKWmU>7BmQd3#AkE;f4(@D*<$vrYl5EYcv-E zLaV%2SID-bJTV&0UbmEwTF%y}E`2ao}8`7Nv1!U5eLF}xL0DeNwgX5JUQ~zq>ZGKZy5Y#Bg*I3{#AOS$iv%`^f5juZHdH?abZ|3yF$c;(AcvL5LD%EOzl~KC9fdmj#Ps$2(fNZ_UrL74tz9?8EsAJi zHk`!`%88SXf8+rQSHX#v1<@$2JyG`o2Y3EGi048hXOJJ$cejep&~9P`e@=f!db;0{ z0E}8FZz z=GKjKNhbS85Iv;Up;13S@Xr)SyChOlN0Ba^1kkR?mp1ljH-s6j=7z({3fWVhV9L#EC$?t>yevmfi3)JH%{CppVuGTlYZcP7V{-opOj0hRoC`V)ETXi!!Bk z8zt_{&n)YYm|J6XX@$5)VgVXupEjyPx}aa~)HG8l)ly^|J}wHhVXAj6I?1&@?b~$( zN{4`w$bDJO`I%Ta~MuRMe?H;CZzG( z5uLAa@A^x=!nd!)rHW_^WI~ZUYXLL1bfiE%9r8!x_9$@EoIR3jI#e@%&)HPUGtHz* zHym^%GSVyW$uMFq(OSmKjzsZ28xa4+b{m@mU&OKdN+fT2GYTVzZwgn6Y=#jr1YdCj zwhh*vx{$NoQE>ypH`>{}8p W{v)7vvw!ahJarXq*hi>!ETa%9R+H*<`U>&4aVKa z9RQvb8fn7ULdWalyaVt60PA?a>2+csMgRbO&k0ZWGv9>BhNNGOpKCnAgPS}t*^pbb zj+mSq3>SRGD%cx%E2jGXLa3>C-}vHoQutn-oD2oCvNi+&@6AX6IC6ud2LPu%VE|BM zjRSzRe--Nkz>hiR0AP7c1exwxHC-jJN??`1DuGo3s|5ZB1k_6_8ggq=s#{9OJE1Tn zG&F=(R#qmYhRE{@>E`T-357x?lhuX4;djyf!#bp`Z(+v7X{D6>g+LdWF|8zDY;)ppb`e=F|IF%HHHS9-t zZ$Y+cUE@WpSOttrDD6(jv^p)KxIQZiE}+l&x7UCtM&GD?O)EW(W0A)%cCZaCE8m94+vbA zZC%pj#w_n*{Je>WQokDHDVqA2*OhC6DrPMQAdFB`kg;Ap~65gypMDQ~!h* zyA#iW`^HMoa9*^4J+qf;Y;b2CoU@*Ka%XE^eKX_*X()v>MAo~D9ps`M%E?`rX+GP* zY0%!0TwmYVcBM0}jAJIOL)xj=^&>7rw1+lEGP)|Y3E8Y;9sGdui@`(|oc2tSxDDCQ zoG8+MFC|y-+)4?{<<(fCHr)YQ{I1$P62{$ZR(N($YQ%7UrRr{F%ck1!WMxU~+s2fb zxH|vl(IV|*DfwGo6xVfy_cG5s%+}hlsYG#{QJ>A4>rRn`a#R=I(I=|h1TjUMtTDlb zExI1K@G;X4IoQ65jxw>qrMZ~pBTGO_Ql=I5%DdF0RAy5=R3{ZW^Qo2cA%lUqY|g!e z;Nt}FlSX(gB1wye3+1X#_Edp>wL<*CYE$}LUvsKo z)5lMS3ag>mUcF*B*gi6Y#*&t-NH$Wibo5IEUc2bV5NKEQiO@iKb9M30?=;Ga9Irk! z3OdjgGB);wN;DQ@imu8_+~0)iAL2^Axj2Vn9+M|OJdF@F?{lC99 zz}VonwP;)$13^@`8sK6dh4p=%VoOyAkw-$UAq{5WGve5i8gtX_eR!^~Xs_Z6DR}#$ zu$!s31yOr1z?)n!wGR?f2?;s-;`X$9*_8(L6sSI!$98a-PhHICGpoV!MkW#Olw)6b z(#5H9@-Tu6E7|zi9!iV+-1SqRy*-uOdG7#PBvU+Gq>jkLqL6!pO;z9fzU41f2U78==_zR)4=nbI_5*#2QW_^dFGY(}nl!Q!>q z;bx**aueb(CGzDg4`pTzh)Nwu`f^yK(B+m58>_OQ>3Zdwhxz##2k*$2Z}A(I!)@X7 z=cZ)}|1Q+n!6t?}ml9kBqD{vo89l>$t#E18khx30=FG~F=sSYuDV1J&gOb3qxG*s> zp;DboRrB70FZS@HOrAFbrNt7oeRedJ~KK9WqkU3SaDHlH#8hy3~H_cHB2ci#;j=0-;;&&s^QI>dofmX$oO`o&$Wo zAu639HE56K(|+SUClDjzF^-J*mvwz$;vb#5O7hvy8(o7Pe>v4nM7RhdIDDr#-8tKJ zlT2Pfb^Q638x(vxgRUUg^4P~dwD$K7n#o_+%RA`yyfTZ}{W(MN&Fla%#iGNQrXzjv z3l?GFtdq|DWYme-M`4qrucs%Vi}4jVmg;pNRNcPND)^MUW3M03@oP}zic2#0=(9$y zRN4>8@q;sYOLobw*JEA?V{ajEzl?J#JBivI@6;+`rp^V(_TUP3NJ0Bz{pmX#y*_11 zYQUVm65NzDtZbUMY)CX>xtOluqX}Ci4O82EJ|2C;7z`Ad%Tl%EClhQvhmUY+wi>g= zK62ieOwrlhG-K&-;GEq~f19HisU!Y{ZEbSMQ1meACZ*hs4$>hyeo+2$_|mQy7uASI zunXhNq`S=6uD9?9Ks8=W8MyulboR9&;hMGpQg%QeU6S=)nS1?V;GAJ##gZbq|CVsE3= zJsj`yofaTtl-qdoc^~5Vv=69o_YH3@+ow}e>0ypWuF2d&>U{seiTyldmqf%UQsON;J z+X@GyLTRLnL5flq+Xmfdb;}vOI=NVoslN}e$~59`LE>}F(-80R@{e<0k~Z)4$r!I( zL3|&}KA#wgsI225b16tqSex;CdZnJ+&`!gf7G?JnlVb;YlHb|t<*_;bVcIW=gDb_< zozz6V*Di0Vj_6de$5b%lHqZgJt>v literal 0 HcmV?d00001 diff --git a/frontend/img/logo.png b/frontend/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..04f8d6df7037631f79e5671357e6f3b8b9e8474b GIT binary patch literal 482 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1SD@HPT%Jfy(mFn2X`sob(e$+nd( zE&SO@PE}vO^nE|A!sO4f_m@RokLr?*Ehf_@922?};Ux86Y09B}wjH_sQ~t8;3CJ)z zAtn@>*Dij`Gvz_F-=2V!o&%c8PG8_s3JP96WA};;?G<(hvoCNldF?dHk<3zBekI!@ zP~O4NS2IE6mD}T~-l?yn$`fzwYjFPc{k4b)S43;XVok+0X&Pb%+*^Mx*v?rK;W)iF zQTK5}@Qj6=tTIju91>ocp1IzzD40V~CBunvdVnq4nVWrUAI#Vs4SL*+}G?>MEVeV(}W;48MDtqH1|1KwwxecmJxli~C7`aZtg zO-7%cD(n6j^d~mkI2lfzSGr(-&5st1V*dZySLScMu;TKYwr`p@T6c7tH7;G)D5=O2 z*K>Qf%e#WU?^e9mW{RKcS$_O)!PDtKW literal 0 HcmV?d00001 diff --git a/frontend/img/potato.png b/frontend/img/potato.png new file mode 100644 index 0000000000000000000000000000000000000000..4387368dbe2e26091fc842448f91c741bdb8611f GIT binary patch literal 1531 zcma)+`!^E`0LN!c+Cs6BDP$^-r6R9GvYA&m!)!EnF)|ucSG&zylPjT7iYY3~+a^pS zk;lBIyz+{ux$>@+9ZFr~>JPZ*+#kN*@A-Vs`TqJ%amP9-g5h8Q0HEmNjKOVZ;4div zw|$EVl-&*)I?f3Vc+$6TaU0}ULI+b_vcYVH7y~~qpCUDnEfg(s{Zho$u-}HYrk90 zJF=uP^Lgus`@8)}q!}AV9ui zYh`t>SuP>nEHW@6!aMFt9;qNM!8#?2Zuf^81QQ%SYDN*F$KEyb2y~iaL_xmdPiF1O?4)Y>-$D zkZBd&6RG@mxJ|t3(D3T&pWG3ZI-~l1S_ZsrL)0sVSGeIWz z1Wip%)zuvN%R6D{;3tZTil`#^*loKY|Iv(bXgO1cX<~Mk z(&*k^gj<}MnR)Pfezk1x_xT^%+Awi++^0>)f`q@NQfYfTruuAQ|2Mr+6AYH4Hmt*L zyV42rPTY%fNoTQMgU-Xw$MS)6Kbw&^tYj9O%jH5RG;dS}_xAQuJ)h<5ybpZz9*)9# z(8TotnMr+-42I+J%Z9g?Ao*;cIhrbVALT-=A+IDpuh})6pHEj*R`#SBv_=JS2#cSY zi^>yrsj4LzpBP3T7ZyacrI8RbMbMaq5{2UeDsE0N2km=_{!l2`<;>vdBpDGeEHMM{ ze)4Bzy!orKH;XSebD{?K{h)njE1&wQq`imhYHJxa%Q%*OD}kd^T_juXmMbhTFDd&FTSgkmzCKSiNi9V3ju7-@yynMN3q!;RE6pWXwn#(v z#1-R@u4k=tu~qbe`MU0&>wk$f+OoKG)wkoNGTL-EN0eK8V2TBjo;1$VH|n?bvI?M- zvI_v#*5JWi5f0!$69~1)b+ImopL`QZ*XrryEM$p9@Mlkj_)=XZAirnSR-Zc3g;$V+ zZ?p&)`3*7LkwJ@-s~DXc`6u$?Mrl&2alZl~5QQhsQN=AI`I})h44$|NyLv5O@DXzJ zoeC%$BOy+zuC>=&ub6&i;mYvbkpcj-#CqzYy;1=u!ASLvGQM8Q8IQt-uiRq-gWO<%32CQWj*J^*hUPr9`0gGz=hu8A2k5bOUBZ~yfN&qyVLF)O zB>>~#BAiF!1%Xl@N2!vc#7 z%iRzNh?SX{8AYqJtFlL6po4%6gjF8cvSuQ}a|lzSMkC>E1D!)@3f^AEY=6m@;O4Sq z-DQT<2>P?N6*V=tC!&~Hh1A6mEol5o(-a_?m7Z0C@Z6FmVq0TpraS#rEro6#r}_`} zQK;_wCnG$CC==a`;hHXVEFWFf-!zuFa5`_5$IFx=;KcuVZNxjTHg^QptHy8kN$s}} O8{p!A#XLciul)-@BG(uI literal 0 HcmV?d00001 diff --git a/frontend/pages/RecommendationHistoryPage.py b/frontend/pages/RecommendationHistoryPage.py deleted file mode 100644 index 51d8b6d..0000000 --- a/frontend/pages/RecommendationHistoryPage.py +++ /dev/null @@ -1,89 +0,0 @@ -import time, math - -import streamlit as st -import pandas as pd -import numpy as np - -import requests - -from utils import page_header, get_response, patch_feedback - -def show_feedback_button(recipe_id, user_feedback): - - icon_mapper = lambda cooked: '❤️' if cooked else '🩶' - cooked = recipe_id in user_feedback - - st.button( - icon_mapper(cooked), - on_click=patch_feedback, - key=f'{recipe_id}_feedback_button', - args=(st.session_state.user, recipe_id, cooked)) - -def display_recipes_in_rows_of_four(recipe_list, user_feedback=None): - - for row in range(math.ceil(len(recipe_list)/4)): - cols = st.columns(4) - - for i in range(4): - item_idx = i + row * 4 - if item_idx >= len(recipe_list): break - - item = recipe_list[item_idx] - with cols[i]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - if user_feedback is None: - st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) - else: - sub_cols = st.columns([3,1]) - with sub_cols[0]: - st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) - with sub_cols[-1]: - show_feedback_button(item['recipe_id'], user_feedback) - - -def get_and_stack_recipe_data(): - - url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/recommended?page={page_num}" - recipe_list, user_feedback = [], [] - formatted_url = url.format(user_id=st.session_state.user, page_num=1) - - while formatted_url: - print(formatted_url) - data = get_response(formatted_url) - recipe_list.extend(data['recipe_list']) - formatted_url = data['next_page_url'] - print(data['user_feedback']) - user_feedback = data['user_feedback'] - - return recipe_list, user_feedback - -def recommendation_history_page(): - - # 앱 헤더 - page_header() - - # get data - recipe_list, user_feedback = get_and_stack_recipe_data() - -# url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/recommended?page={page_num}" -# user_id, page_num = 1,1 -# formatted_url = url.format(user_id=user_id, page_num=page_num) -# data = get_response(formatted_url) - - # 페이지 구성 - container = st.container(border=True) - - with container: - - st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) - - sub_container = st.container(border=False) - with sub_container: - st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) - st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) - - display_ingredients_in_rows_of_four(recipe_list, user_feedback) - -st.session_state['page_info'] = 'recommend_history' -recommendation_history_page() diff --git a/frontend/pages/RecommendationPage.py b/frontend/pages/RecommendationPage.py deleted file mode 100644 index ee28979..0000000 --- a/frontend/pages/RecommendationPage.py +++ /dev/null @@ -1,51 +0,0 @@ -import streamlit as st -import pandas as pd -import numpy as np -import time - -from utils import page_header, basket_feedback -from pages.ResultPage2 import result_page_2 - -def recommendation_page(): - - # 앱 헤더 - page_header() - - # 페이지 구성 - container = st.container(border=True) - - with container: - st.markdown("

이번 주 장바구니 만들기

", unsafe_allow_html=True) - st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) - st.markdown("
예산을 정해주세요.
", unsafe_allow_html=True) - - cols = st.columns([1,5,1]) - - with cols[1]: - - price = st.slider( - label='', min_value=10000, max_value=1000000, value=50000, step=5000 - ) - - cols = st.columns(5) - - with cols[2]: - st.write("예산: ", price, '원') - - - cols = st.columns(3) - - with cols[1]: - button2 = st.button("장바구니 추천받기", type="primary") - if button2: - st.session_state['page_info'] = 'result_page_1' - - -# 이전 추천 목록 조회 -if 'page_info' not in st.session_state: - st.session_state['page_info'] = 'recommend' - -if st.session_state['page_info'] == 'result_page_1': - result_page_2() -else: - recommendation_page() diff --git a/frontend/pages/ResultPage2.py b/frontend/pages/ResultPage2.py deleted file mode 100644 index 722a256..0000000 --- a/frontend/pages/ResultPage2.py +++ /dev/null @@ -1,69 +0,0 @@ -import time, math - -import pandas as pd -import numpy as np - -from PIL import Image -import streamlit as st -import requests - -from utils import page_header, basket_feedback, get_response, patch_feedback -from pages.RecommendationHistoryPage import display_ingredients_in_rows_of_four - -def display_ingredients_in_rows_of_four(ingredients): - for ingredient in ingredients: - sub_container = st.container(border=True) - - with sub_container: - - cols = st.columns(5) - - with cols[0]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - with cols[1]: - st.write(ingredient['ingredient_name']) - st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) - - with cols[-1]: - st.link_button('구매', ingredient['market_url'], type='primary') - -def result_page_2(): - - # 앱 헤더 - page_header() - - url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/previousrecommendation" - formatted_url = url.format(user_id=st.session_state.user) - data = get_response(formatted_url) - - # 페이지 구성 - container = st.container(border=True) - - with container: - - # 장바구니 추천 문구 - st.markdown("

새로운 장바구니를 추천받았어요!

", unsafe_allow_html=True) - st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) - - st.divider() - - # 구매할 식료품 목록 - st.markdown("

추천 장바구니

", unsafe_allow_html=True) - - display_ingredients_in_rows_of_four(data['ingredient_list']) - total_price = sum([ingredient['ingredient_price'] for ingredient in data['ingredient_list']]) - - st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) - - st.divider() - - # 이 장바구니로 만들 수 있는 음식 레시피 - st.markdown("

이 장바구니로 만들 수 있는 음식 레시피

", unsafe_allow_html=True) - display_ingredients_in_rows_of_four(data['recipe_list']) - - st.text("\n\n") - basket_feedback() - -st.session_state['page_info'] = "result_page_2" -result_page_2() diff --git a/frontend/pages/UserHistoryPage.py b/frontend/pages/UserHistoryPage.py deleted file mode 100644 index 638ebf1..0000000 --- a/frontend/pages/UserHistoryPage.py +++ /dev/null @@ -1,43 +0,0 @@ -import time, math - -import pandas as pd -import numpy as np - -import streamlit as st -import requests - -from utils import page_header, get_response, patch_feedback -from pages.RecommendationHistoryPage import display_ingredients_in_rows_of_four - -def get_and_stack_recipe_data(): - - url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/cooked?page={page_num}" - recipe_list = [] - formatted_url = url.format(user_id=st.session_state.user, page_num=1) - - while formatted_url: - data = get_response(formatted_url) - recipe_list.extend(data['recipe_list']) - formatted_url = data['next_page_url'] - - return recipe_list - -def user_history_page(): - - # 앱 헤더 - page_header() - - # get data - recipe_list = get_and_stack_recipe_data() - - # show container - container = st.container(border=True) - - with container: - # title - st.markdown("

❤️ 내가 요리한 레시피 ❤️

", unsafe_allow_html=True) - display_ingredients_in_rows_of_four(recipe_list) - -# show UserHistoryPage -st.session_state['page_info'] = 'user_history' -user_history_page() diff --git a/frontend/requirements.txt b/frontend/requirements.txt new file mode 100644 index 0000000..6f70eeb --- /dev/null +++ b/frontend/requirements.txt @@ -0,0 +1,5 @@ +streamlit +supabase +st_login_form +st-supabase-connection +argon2 \ No newline at end of file diff --git a/frontend/utils.py b/frontend/utils.py index ff3d9c0..e69de29 100644 --- a/frontend/utils.py +++ b/frontend/utils.py @@ -1,74 +0,0 @@ -import streamlit as st -import requests - -def set_logout(): - st.session_state.user = None - st.session_state.is_authenticated = False - -def set_login(): - st.session_state.user = 'judy123' - st.session_state.is_authenticated = True - -def login_button(): - if st.session_state.is_authenticated: - login_button = st.button(f"{st.session_state.user}님 | 로그아웃", on_click=set_logout) - else: - login_button = st.button(f"회원가입 | 로그인", on_click=set_login) - - return login_button - -def page_header(): - cols = st.columns([8,3]) - with cols[0]: - st.header('나만의 식량 바구니') - with cols[-1]: - login_button() - button_css() - -def basket_feedback(): - st.markdown("
방금 추천받은 장바구니 어땠나요?
", unsafe_allow_html=True) - st.text("") - cols = st.columns([3,1,1,3]) - with cols[1]: - st.button('좋아요') - with cols[2]: - st.button('싫어요') - -def get_response(formatted_url): - response = requests.get(formatted_url) - if response.status_code == 200: - data = response.json() - else: - print(f'status code: {response.status_code}') - data = None - return data - -def patch_feedback(user_id, recipe_id, current_state): - url = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/users/{user_id}/recipes/{recipe_id}/feedback" - data = { - 'feedback': not current_state - } - response = requests.patch(url.format(user_id=user_id, recipe_id=recipe_id), json=data) - print(f'status code: {response.status_code}') - st.rerun() - -def button_css(): - st.markdown( - """""", - unsafe_allow_html=True, - ) - -# button[kind="primary"] { -# } -# button[kind="seondary"] { -# div.stButton button { -# width: 150px; -# border: none !important; -# } From cbac71c611edd8e7cc67de61dbb1d55eb5c0459b Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Fri, 15 Mar 2024 17:43:36 +0900 Subject: [PATCH 079/187] Update requirements.txt --- frontend/requirements.txt | 155 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 150 insertions(+), 5 deletions(-) diff --git a/frontend/requirements.txt b/frontend/requirements.txt index 6f70eeb..b7cce8c 100644 --- a/frontend/requirements.txt +++ b/frontend/requirements.txt @@ -1,5 +1,150 @@ -streamlit -supabase -st_login_form -st-supabase-connection -argon2 \ No newline at end of file +altair==5.2.0 +annotated-types==0.6.0 +anyio==4.3.0 +argon2-cffi==23.1.0 +argon2-cffi-bindings==21.2.0 +attrs==23.2.0 +beautifulsoup4==4.12.3 +blinker==1.7.0 +Bottleneck==1.3.7 +Brotli==1.0.9 +cachetools==5.3.3 +certifi==2024.2.2 +cffi==1.16.0 +charset-normalizer==3.3.2 +click==8.1.7 +contourpy==1.2.0 +cryptography==42.0.5 +cycler==0.12.1 +deprecation==2.1.0 +entrypoints==0.4 +exceptiongroup==1.2.0 +extra-streamlit-components==0.1.70 +Faker==24.1.0 +favicon==0.7.0 +filelock==3.13.1 +Flask==3.0.2 +fonttools==4.49.0 +fsspec==2024.2.0 +gitdb==4.0.11 +GitPython==3.1.42 +gmpy2==2.1.2 +gotrue==2.4.1 +h11==0.14.0 +htbuilder==0.6.2 +httpcore==1.0.4 +httpx==0.25.2 +idna==3.6 +itsdangerous==2.1.2 +Jinja2==3.1.3 +jsonschema==4.21.1 +jsonschema-specifications==2023.12.1 +kiwisolver==1.4.5 +lxml==5.1.0 +Markdown==3.5.2 +markdown-it-py==3.0.0 +markdownlit==0.0.7 +MarkupSafe==2.1.5 +matplotlib==3.8.3 +mdurl==0.1.2 +mkl-fft==1.3.8 +mkl-random==1.2.4 +mkl-service==2.4.0 +more-itertools==10.2.0 +mpmath==1.3.0 +networkx==3.2.1 +numexpr==2.8.7 +numpy==1.26.4 +nvidia-cublas-cu12==12.1.3.1 +nvidia-cuda-cupti-cu12==12.1.105 +nvidia-cuda-nvrtc-cu12==12.1.105 +nvidia-cuda-runtime-cu12==12.1.105 +nvidia-cudnn-cu12==8.9.2.26 +nvidia-cufft-cu12==11.0.2.54 +nvidia-curand-cu12==10.3.2.106 +nvidia-cusolver-cu12==11.4.5.107 +nvidia-cusparse-cu12==12.1.0.106 +nvidia-nccl-cu12==2.19.3 +nvidia-nvjitlink-cu12==12.3.101 +nvidia-nvtx-cu12==12.1.105 +outcome==1.1.0 +packaging==23.2 +pandas==2.2.1 +pillow==10.2.0 +pip==24.0 +postgrest==0.16.1 +prometheus_client==0.20.0 +protobuf==4.25.3 +pyarrow==15.0.1 +pycparser==2.21 +pydantic==2.6.3 +pydantic_core==2.16.3 +pydeck==0.8.1b0 +Pygments==2.17.2 +pymdown-extensions==10.7.1 +pyparsing==3.1.2 +PySocks==1.7.1 +python-dateutil==2.9.0.post0 +python-decouple==3.8 +python-dotenv==1.0.1 +pytz==2024.1 +PyYAML==6.0.1 +realtime==1.0.2 +referencing==0.33.0 +requests==2.31.0 +rich==13.7.1 +rpds-py==0.18.0 +selenium==4.18.1 +setuptools==69.1.1 +six==1.16.0 +smmap==5.0.1 +sniffio==1.3.0 +sortedcontainers==2.4.0 +soupsieve==2.5 +st-annotated-text==4.0.1 +st-login-form==0.2.2 +st-supabase-connection==1.2.2 +storage3==0.7.3 +streamlit==1.32.0 +streamlit-aggrid==0.3.4.post3 +streamlit-antd-components==0.3.2 +streamlit-camera-input-live==0.2.0 +streamlit-card==1.0.0 +streamlit-cookies-manager==0.2.0 +streamlit-embedcode==0.1.2 +streamlit-extras==0.4.0 +streamlit-faker==0.0.3 +streamlit-image-coordinates==0.1.6 +streamlit-keyup==0.2.3 +streamlit-login-auth-ui==0.2.0 +streamlit-lottie==0.0.5 +streamlit-option-menu==0.3.12 +streamlit-toggle-switch==1.0.2 +streamlit-vertical-slider==2.5.5 +StrEnum==0.4.15 +supabase==2.4.0 +supafunc==0.3.3 +sympy==1.12 +tenacity==8.2.3 +toml==0.10.2 +toolz==0.12.1 +torch==2.2.1 +torchaudio==2.2.1 +torchdata==0.7.1 +torchtext==0.17.1 +torchvision==0.17.1 +tornado==6.4 +tqdm==4.65.0 +trio==0.24.0 +trio-websocket==0.11.1 +triton==2.2.0 +trycourier==5.0.0 +typing_extensions==4.10.0 +tzdata==2024.1 +urllib3==2.2.1 +watchdog==4.0.0 +webdriver-manager==4.0.1 +websockets==11.0.3 +Werkzeug==3.0.1 +wheel==0.41.2 +wsproto==1.2.0 From 45bb7e83fc6821029d6a6cda175f4115a7ae8923 Mon Sep 17 00:00:00 2001 From: GangBean Date: Fri, 15 Mar 2024 19:21:21 +0900 Subject: [PATCH 080/187] =?UTF-8?q?fix:=20=EB=82=B4=EA=B0=80=20=EC=9A=94?= =?UTF-8?q?=EB=A6=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C=ED=94=BC=20API=20404=20?= =?UTF-8?q?Not=20Found=20=EC=88=98=EC=A0=95=20#37?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/README.xxx | 0 .../app/api/routes/recipes/controller/recipes_controller.py | 6 +++--- backend/app/api/routes/recipes/dto/get_recipes_reponse.py | 2 +- backend/app/api/routes/recipes/entity/ingredient.py | 2 +- backend/app/api/routes/recipes/entity/recipe.py | 2 +- backend/app/api/routes/recipes/entity/user.py | 2 +- .../app/api/routes/recipes/repository/recipes_repository.py | 2 +- backend/app/app.py | 2 ++ 8 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 backend/README.xxx diff --git a/backend/README.xxx b/backend/README.xxx new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/routes/recipes/controller/recipes_controller.py b/backend/app/api/routes/recipes/controller/recipes_controller.py index fd48361..2f793d5 100644 --- a/backend/app/api/routes/recipes/controller/recipes_controller.py +++ b/backend/app/api/routes/recipes/controller/recipes_controller.py @@ -8,7 +8,7 @@ recipes_service = RecipesService() @recipes_router.get( - "/users/{user_id}/recipes/cooked", + "/api/users/{user_id}/recipes/cooked", response_description="유저가 요리한 레시피 목록" ) def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): @@ -24,7 +24,7 @@ def get_user_cooked_recipes_by_page(user_id: str, page_num: int = 0): @recipes_router.get( - "/users/{user_id}/recipes/recommended", + "/api/users/{user_id}/recipes/recommended", response_description="유저가 추천받은 레시피 목록" ) def get_user_recommended_recipes_by_page(user_id: str, page_num: int = 0): @@ -41,7 +41,7 @@ def get_user_recommended_recipes_by_page(user_id: str, page_num: int = 0): return GetRecipesReponseList(recipes, ingredients_list, user_cooked_recipes_id) -@recipes_router.patch("/users/{user_id}/recipes/{recipes_id}/feedback") +@recipes_router.patch("/api/users/{user_id}/recipes/{recipes_id}/feedback") def update_user_recipes_status__by_feedback(user_id: str, recipes_id: str, request: RecipeStatusUpadateRequest): update_result = recipes_service.update_cooked_recipes(user_id, recipes_id, request.feedback) if update_result: diff --git a/backend/app/api/routes/recipes/dto/get_recipes_reponse.py b/backend/app/api/routes/recipes/dto/get_recipes_reponse.py index 49c8412..07e8872 100644 --- a/backend/app/api/routes/recipes/dto/get_recipes_reponse.py +++ b/backend/app/api/routes/recipes/dto/get_recipes_reponse.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from .....utils.pyobject_id import PyObjectId +from utils.pyobject_id import PyObjectId from typing import Dict diff --git a/backend/app/api/routes/recipes/entity/ingredient.py b/backend/app/api/routes/recipes/entity/ingredient.py index 5b850fc..66922bf 100644 --- a/backend/app/api/routes/recipes/entity/ingredient.py +++ b/backend/app/api/routes/recipes/entity/ingredient.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field, ConfigDict -from .....utils.pyobject_id import PyObjectId +from utils.pyobject_id import PyObjectId class Ingredient(BaseModel): diff --git a/backend/app/api/routes/recipes/entity/recipe.py b/backend/app/api/routes/recipes/entity/recipe.py index 72e0080..4b97345 100644 --- a/backend/app/api/routes/recipes/entity/recipe.py +++ b/backend/app/api/routes/recipes/entity/recipe.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field, ConfigDict -from .....utils.pyobject_id import PyObjectId +from utils.pyobject_id import PyObjectId from typing import List diff --git a/backend/app/api/routes/recipes/entity/user.py b/backend/app/api/routes/recipes/entity/user.py index 40d7286..29d6939 100644 --- a/backend/app/api/routes/recipes/entity/user.py +++ b/backend/app/api/routes/recipes/entity/user.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, Field, ConfigDict from typing import List -from .....utils.pyobject_id import PyObjectId +from utils.pyobject_id import PyObjectId class User(BaseModel): diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index 4611dbb..fe18b66 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -1,7 +1,7 @@ from fastapi import HTTPException from bson import ObjectId from typing import List -from .....database.data_source import data_source +from database.data_source import data_source from ..entity.user import User from ..entity.recipes import Recipes from ..entity.ingredients import Ingredients diff --git a/backend/app/app.py b/backend/app/app.py index 4ba583e..6e0bedf 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -1,6 +1,7 @@ import uvicorn from fastapi import FastAPI, APIRouter from api.routes.users.controller.user_controller import user_router +from api.routes.recipes.controller.recipes_controller import recipes_router def new_app() -> FastAPI: return FastAPI() @@ -15,6 +16,7 @@ def hello(): app.include_router(router) app.include_router(user_router) +app.include_router(recipes_router) if __name__ == '__main__': uvicorn.run(app, host='0.0.0.0') From 8bc2218efc704e96cdd36c1d2d31fc5fe073e454 Mon Sep 17 00:00:00 2001 From: Judy Date: Sat, 16 Mar 2024 20:01:27 +0900 Subject: [PATCH 081/187] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=EC=97=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/common.py | 40 ++++++++++++++++++++++++++++++++++++++++ frontend/newapp.py | 9 +++++++++ frontend/utils.py | 0 3 files changed, 49 insertions(+) create mode 100644 frontend/common.py create mode 100644 frontend/newapp.py delete mode 100644 frontend/utils.py diff --git a/frontend/common.py b/frontend/common.py new file mode 100644 index 0000000..ed7a8b0 --- /dev/null +++ b/frontend/common.py @@ -0,0 +1,40 @@ +import streamlit as st + +def init(): + st.session_state.is_authenticated = False + +def set_logout(): + st.session_state.user = None + st.session_state.is_authenticated = False + +def set_login(): + st.session_state.user = 'judy123' + st.session_state.is_authenticated = True + +def login_button(): + if st.session_state.is_authenticated: + login_button = st.button(f"{st.session_state.user}님 | 로그아웃", on_click=set_logout) + else: + login_button = st.button(f"회원가입 | 로그인", on_click=set_login) + return login_button + +def page_header(): + cols = st.columns([8,3]) + with cols[0]: + st.header('나만의 식량 바구니') + with cols[-1]: + login_button() + button_css() + +def button_css(): + st.markdown( + """""", + unsafe_allow_html=True, + ) diff --git a/frontend/newapp.py b/frontend/newapp.py new file mode 100644 index 0000000..0e07e15 --- /dev/null +++ b/frontend/newapp.py @@ -0,0 +1,9 @@ +import streamlit as st + +from common import init, page_header + +# 페이지 초기화 +init() + +# 일단 헤더를 띄운다 +page_header() diff --git a/frontend/utils.py b/frontend/utils.py deleted file mode 100644 index e69de29..0000000 From 56616e4644088bb1bcf963c2bff73fd708bf66f6 Mon Sep 17 00:00:00 2001 From: Judy Date: Sat, 16 Mar 2024 20:56:42 +0900 Subject: [PATCH 082/187] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B3=80=EA=B2=BD=20&=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EA=B0=80=EC=9E=85,=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84=20&=20=EC=9B=B9=20=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=20=ED=81=B4=EB=A6=AD=20=EC=8B=9C=20=EC=83=88=EB=A1=9C?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD#41=20#42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/basket_login.py | 78 +++++++++++++++++++++++++++++++++++++ frontend/basket_signup.py | 82 +++++++++++++++++++++++++++++++++++++++ frontend/common.py | 60 ++++++++++++++++++++++------ frontend/newapp.py | 33 ++++++++++++++-- 4 files changed, 237 insertions(+), 16 deletions(-) create mode 100644 frontend/basket_login.py create mode 100644 frontend/basket_signup.py diff --git a/frontend/basket_login.py b/frontend/basket_login.py new file mode 100644 index 0000000..56b2f83 --- /dev/null +++ b/frontend/basket_login.py @@ -0,0 +1,78 @@ +import streamlit as st +import requests + +def login_request(username, password): + + full_url = st.session_state.url_prefix + '/api/users/auths' + + request_body = { + 'login_id': username, + 'password': password, + } + + headers = { + 'Content-Type': 'application/json' + } + + response = requests.post(full_url, headers=headers, json=request_body) + response_json = response.json if response.status_code == 200 else None + + return response.status_code, response_json + +def check_password(): + """Returns `True` if the user had a correct password.""" + + def login_form(): + """Form with widgets to collect user information""" + with st.form("Credentials"): + st.text_input("Username", key="username") + st.text_input("Password", type="password", key="password") + st.form_submit_button("Log in", on_click=password_entered) + + def password_entered(): + """Checks whether a password entered by the user is correct.""" + # api 기준으로 바꿔보자 + input_username = st.session_state['username'] + input_password = st.session_state['password'] + + status_code, response = login_request(input_username, input_password) + + if status_code == 200: + + st.session_state["password_correct"] = True + st.session_state["token"] = response['token'] + + del st.session_state["password"] # Don't store the username or password. + + else: + + st.session_state["password_correct"] = False + print(status_code) + + if status_code == 400: + st.session_state.msg = "password incorrect" + elif status_code == 404: + st.session_state.msg = "User not known" + else: + st.session_state.msg = "Server Error" + + + + # Return True if the username + password is validated. + if st.session_state.get("password_correct", False): + return True + + # Show inputs for username + password. + login_form() + + if "password_correct" in st.session_state: + st.error(f"😕 {st.session_state.msg}") + return False + + +def login_container(): + if not check_password(): + st.stop() + + # Main Streamlit app starts here + st.session_state.page_info = 'home2' diff --git a/frontend/basket_signup.py b/frontend/basket_signup.py new file mode 100644 index 0000000..ece3ed3 --- /dev/null +++ b/frontend/basket_signup.py @@ -0,0 +1,82 @@ +# streamlit_app.py + +import hmac +import streamlit as st +import requests + +def signup_request(username, nickname, email, password): + + full_url = st.session_state.url_prefix + '/api/users' + + request_body = { + 'login_id': username, + 'password': password, + 'nickname': nickname, + 'email': email, + } + + headers = { + 'Content-Type': 'application/json' + } + + response = requests.post(full_url, headers=headers, json=request_body) + response_json = response.json() if response.status_code == 200 else None + + if response.status_code != 200: + st.session_state.msg = eval(response.text)['detail'] + + return response.status_code, response_json + +def check_password(): + """Returns `True` if the user had a correct password.""" + + def signup_form(): + """Form with widgets to collect user information""" + with st.form("Credentials"): + st.text_input("login_id", key="login_id") + st.text_input("nickname", key="nickname") + st.text_input("email", key="email") + st.text_input("password", type="password", key="password") + st.form_submit_button("회원가입", on_click=password_entered) + + def password_entered(): + """Checks whether a password entered by the user is correct.""" + + status_code, response = signup_request( + st.session_state['login_id'], + st.session_state['nickname'], + st.session_state['email'], + st.session_state['password'], + ) + + if status_code == 200: + + st.session_state["signup_success"] = True + del st.session_state["password"] # Don't store the username or password. + del st.session_state["login_id"] # Don't store the username or password. + del st.session_state["nickname"] # Don't store the username or password. + del st.session_state["email"] # Don't store the username or password. + + else: + st.session_state["signup_success"] = False + + # Return True if the username + password is validated. + if st.session_state.get("signup_success", False): + del st.session_state["signup_success"] + return True + + # Show inputs for username + password. + signup_form() + + if "signup_success" in st.session_state: + st.error(f"😕 {st.session_state.msg}") + return False + +def signup_container(): + if not check_password(): + st.stop() + + # Main Streamlit app starts here + st.session_state.page_info = 'home' +# st.write(f"signup succeeded") +# st.button("Click me") diff --git a/frontend/common.py b/frontend/common.py index ed7a8b0..171d3f9 100644 --- a/frontend/common.py +++ b/frontend/common.py @@ -2,26 +2,60 @@ def init(): st.session_state.is_authenticated = False + st.session_state.page_info = 'home' + st.session_state.url_prefix = 'http://localhost:8000' + st.session_state.url_main = 'http://175.45.194.96:8503/' -def set_logout(): - st.session_state.user = None +def set_logout_page(): st.session_state.is_authenticated = False + st.session_state.page_info = 'home' +# st.session_state.user = None +# st.session_state.is_authenticated = False -def set_login(): - st.session_state.user = 'judy123' - st.session_state.is_authenticated = True +def set_login_page(): + st.session_state.page_info = 'login' +# st.session_state.user = 'judy123' +# st.session_state.is_authenticated = True + +def set_signup_page(): + st.session_state.page_info = 'signup' + print('signup_page') + print(st.session_state.page_info) def login_button(): + cols = st.columns(2) if st.session_state.is_authenticated: - login_button = st.button(f"{st.session_state.user}님 | 로그아웃", on_click=set_logout) + with cols[0]: + st.write(f"{st.session_state.user}님") + with cols[1]: + st.button(f"로그아웃", on_click=set_logout_page) else: - login_button = st.button(f"회원가입 | 로그인", on_click=set_login) + with cols[0]: + st.button(f"회원가입", on_click=set_signup_page) + with cols[1]: + st.button(f"로그인", on_click=set_login_page) return login_button def page_header(): - cols = st.columns([8,3]) + cols = st.columns([7,3]) with cols[0]: - st.header('나만의 식량 바구니') + # st.header('나만의 식량 바구니') + st.markdown( + f'

나만의 식량 바구니

', + unsafe_allow_html=True) + + st.markdown( + '''''', + unsafe_allow_html=True) + with cols[-1]: login_button() button_css() @@ -29,12 +63,14 @@ def page_header(): def button_css(): st.markdown( """""", unsafe_allow_html=True, ) + + # border +# button[kind="secondary"] { +# border: none !important; +# } diff --git a/frontend/newapp.py b/frontend/newapp.py index 0e07e15..f9e8b7f 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -1,9 +1,34 @@ import streamlit as st from common import init, page_header +from basket_signup import signup_container +from basket_login import login_container -# 페이지 초기화 -init() +def home(): + page_header() + +def home2(): + page_header() + st.write('login 됨') + +def login(): + page_header() + login_container() + +def signup(): + page_header() + signup_container() + + +if ('is_authenticated' not in st.session_state) and ('page_info' not in st.session_state): + init() + +if (not st.session_state.is_authenticated) and (st.session_state.page_info == 'home'): + home() +elif (not st.session_state.is_authenticated) and (st.session_state.page_info == 'signup'): + signup() +elif (not st.session_state.is_authenticated) and (st.session_state.page_info == 'login'): + login() +elif (st.session_state.is_authenticated) and (st.session_state.page_info == 'home2'): + home2() -# 일단 헤더를 띄운다 -page_header() From f7e3849674cf9e57c328822720479df2e2116546 Mon Sep 17 00:00:00 2001 From: Judy Date: Sat, 16 Mar 2024 21:12:37 +0900 Subject: [PATCH 083/187] =?UTF-8?q?feat:=20login=20=EC=A0=84=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/main.py | 33 +++++++++++++++++++++++++++++++++ frontend/newapp.py | 3 ++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 frontend/main.py diff --git a/frontend/main.py b/frontend/main.py new file mode 100644 index 0000000..c6c1e42 --- /dev/null +++ b/frontend/main.py @@ -0,0 +1,33 @@ +import streamlit as st +from streamlit_extras.stylable_container import stylable_container + +from common import set_login_page, set_signup_page + +def main_page(): + container3_1 = stylable_container( + key="container_with_border", + css_styles=""" + { + border: 1px solid rgba(49, 51, 63, 0.2); + border-radius: 0.5rem; + padding: calc(1em - 1px); + } + """,) + with container3_1: + st.markdown("

나만의 식량 바구니에 \n 오신 것을 환영합니다!

", unsafe_allow_html=True) + st.markdown("

자신의 입맞에 맞는 레시피를 저장하고 \n 이번주에 구매할 식량 바구니를 추천받아보세요

", unsafe_allow_html=True) + + cols = st.columns([4,2,2,3]) + with cols[1]: + st.button(f"회원가입", on_click=set_signup_page, key=f'signup_{st.session_state.page_info}') + with cols[2]: + st.button(f"로그인", on_click=set_login_page, key=f'login_{st.session_state.page_info}', type='primary') + + container3_2 = st.container(border = True) + + with container3_2: + st.markdown("

사용 방법

", unsafe_allow_html=True) + st.markdown("

회원 가입을 했을 때 어떤 기능을 쓸 수 있는지 살펴보는 페이지

", unsafe_allow_html=True) + left_co, cent_co,last_co = st.columns((1, 8, 1)) + with cent_co: + st.image('img/howto.png') diff --git a/frontend/newapp.py b/frontend/newapp.py index f9e8b7f..4f213d4 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -3,9 +3,11 @@ from common import init, page_header from basket_signup import signup_container from basket_login import login_container +from main import main_page def home(): page_header() + main_page() def home2(): page_header() @@ -19,7 +21,6 @@ def signup(): page_header() signup_container() - if ('is_authenticated' not in st.session_state) and ('page_info' not in st.session_state): init() From 18290e5412b41991941b2de3194f067453b85d87 Mon Sep 17 00:00:00 2001 From: Judy Date: Sat, 16 Mar 2024 23:09:17 +0900 Subject: [PATCH 084/187] =?UTF-8?q?feat:=20recommendation=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=20&=20st.session=5Fstat?= =?UTF-8?q?e=EC=97=90=EC=84=9C=20user=5Fid=20=EC=82=AC=EB=9D=BC=EC=A7=80?= =?UTF-8?q?=EB=8A=94=20=EA=B2=83=20token=20=EC=95=88=EC=97=90=20=EB=84=A3?= =?UTF-8?q?=EB=8A=94=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=20=EB=A1=9C=20=ED=94=BD?= =?UTF-8?q?=EC=8A=A4=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/basket_login.py | 29 +++++++++++++----------- frontend/common.py | 20 +++++++++------- frontend/main.py | 8 ++++++- frontend/newapp.py | 14 ++++++++---- frontend/pages/recommendation.py | 27 ++++++++++++++++++++++ frontend/recommendation.py | 39 ++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 26 deletions(-) create mode 100644 frontend/pages/recommendation.py create mode 100644 frontend/recommendation.py diff --git a/frontend/basket_login.py b/frontend/basket_login.py index 56b2f83..17ba16e 100644 --- a/frontend/basket_login.py +++ b/frontend/basket_login.py @@ -1,12 +1,12 @@ import streamlit as st import requests -def login_request(username, password): +def login_request(user_id, password): full_url = st.session_state.url_prefix + '/api/users/auths' request_body = { - 'login_id': username, + 'login_id': user_id, 'password': password, } @@ -15,7 +15,7 @@ def login_request(username, password): } response = requests.post(full_url, headers=headers, json=request_body) - response_json = response.json if response.status_code == 200 else None + response_json = response.json() if response.status_code == 200 else None return response.status_code, response_json @@ -25,24 +25,30 @@ def check_password(): def login_form(): """Form with widgets to collect user information""" with st.form("Credentials"): - st.text_input("Username", key="username") + st.text_input("Username", key="user_id") st.text_input("Password", type="password", key="password") st.form_submit_button("Log in", on_click=password_entered) def password_entered(): """Checks whether a password entered by the user is correct.""" # api 기준으로 바꿔보자 - input_username = st.session_state['username'] + input_user_id = st.session_state['user_id'] input_password = st.session_state['password'] - status_code, response = login_request(input_username, input_password) + status_code, response = login_request(input_user_id, input_password) if status_code == 200: + # 페이지 전환을 위해 + st.session_state.is_authenticated = True st.session_state["password_correct"] = True - st.session_state["token"] = response['token'] + st.session_state.page_info = 'home2' + st.session_state["token"] = { + 'user_id': st.session_state['user_id'], + 'token': response['token'] + } - del st.session_state["password"] # Don't store the username or password. + del st.session_state["password"] # Don't store the user_id or password. else: @@ -58,11 +64,11 @@ def password_entered(): - # Return True if the username + password is validated. + # Return True if the user_id + password is validated. if st.session_state.get("password_correct", False): return True - # Show inputs for username + password. + # Show inputs for user_id + password. login_form() if "password_correct" in st.session_state: @@ -73,6 +79,3 @@ def password_entered(): def login_container(): if not check_password(): st.stop() - - # Main Streamlit app starts here - st.session_state.page_info = 'home2' diff --git a/frontend/common.py b/frontend/common.py index 171d3f9..4d0184d 100644 --- a/frontend/common.py +++ b/frontend/common.py @@ -1,6 +1,12 @@ +import random, string + import streamlit as st +import streamlit_antd_components as sac + +random_chars = lambda: ''.join(random.choices(string.ascii_letters + string.digits, k=5)) def init(): + print('init!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') st.session_state.is_authenticated = False st.session_state.page_info = 'home' st.session_state.url_prefix = 'http://localhost:8000' @@ -9,13 +15,9 @@ def init(): def set_logout_page(): st.session_state.is_authenticated = False st.session_state.page_info = 'home' -# st.session_state.user = None -# st.session_state.is_authenticated = False def set_login_page(): st.session_state.page_info = 'login' -# st.session_state.user = 'judy123' -# st.session_state.is_authenticated = True def set_signup_page(): st.session_state.page_info = 'signup' @@ -26,14 +28,15 @@ def login_button(): cols = st.columns(2) if st.session_state.is_authenticated: with cols[0]: - st.write(f"{st.session_state.user}님") + st.write(f"{st.session_state.token['user_id']}님") with cols[1]: - st.button(f"로그아웃", on_click=set_logout_page) + st.button(f"로그아웃", on_click=set_logout_page, key=f'logout_{st.session_state.page_info}_{random_chars()}') + else: with cols[0]: - st.button(f"회원가입", on_click=set_signup_page) + st.button(f"회원가입", on_click=set_signup_page, key=f'signup_{st.session_state.page_info}_{random_chars()}') with cols[1]: - st.button(f"로그인", on_click=set_login_page) + st.button(f"로그인", on_click=set_login_page, key=f'login_{st.session_state.page_info}_{random_chars()}') return login_button def page_header(): @@ -58,6 +61,7 @@ def page_header(): with cols[-1]: login_button() + button_css() def button_css(): diff --git a/frontend/main.py b/frontend/main.py index c6c1e42..56cfa0b 100644 --- a/frontend/main.py +++ b/frontend/main.py @@ -3,7 +3,7 @@ from common import set_login_page, set_signup_page -def main_page(): +def welcome_container(): container3_1 = stylable_container( key="container_with_border", css_styles=""" @@ -23,6 +23,7 @@ def main_page(): with cols[2]: st.button(f"로그인", on_click=set_login_page, key=f'login_{st.session_state.page_info}', type='primary') +def howto_container(): container3_2 = st.container(border = True) with container3_2: @@ -31,3 +32,8 @@ def main_page(): left_co, cent_co,last_co = st.columns((1, 8, 1)) with cent_co: st.image('img/howto.png') + +def main_page(): + + welcome_container() + howto_container() diff --git a/frontend/newapp.py b/frontend/newapp.py index 4f213d4..807aa35 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -4,6 +4,7 @@ from basket_signup import signup_container from basket_login import login_container from main import main_page +from recommendation import recommendation_page def home(): page_header() @@ -21,15 +22,20 @@ def signup(): page_header() signup_container() +def recommendation(): + page_header() + recommendation_page() + if ('is_authenticated' not in st.session_state) and ('page_info' not in st.session_state): init() -if (not st.session_state.is_authenticated) and (st.session_state.page_info == 'home'): +value = st.session_state.get('key', 'default_value') +if (not st.session_state.get('is_authenticated', False)) and (st.session_state.get('page_info', '-') == 'home'): home() -elif (not st.session_state.is_authenticated) and (st.session_state.page_info == 'signup'): +elif (not st.session_state.get('is_authenticated', False)) and (st.session_state.get('page_info', '-') == 'signup'): signup() -elif (not st.session_state.is_authenticated) and (st.session_state.page_info == 'login'): +elif (not st.session_state.get('is_authenticated', False)) and (st.session_state.get('page_info', '-') == 'login'): login() -elif (st.session_state.is_authenticated) and (st.session_state.page_info == 'home2'): +elif st.session_state.get('is_authenticated', False) and (st.session_state.get('page_info', '-') == 'home2'): home2() diff --git a/frontend/pages/recommendation.py b/frontend/pages/recommendation.py new file mode 100644 index 0000000..625a0dc --- /dev/null +++ b/frontend/pages/recommendation.py @@ -0,0 +1,27 @@ +import streamlit as st + +from common import init, page_header +from newapp import recommendation +from main import welcome_container +#from recommendation import recommendation_page + +if 'is_authenticated' not in st.session_state: + init() + print('page_recommendation') + st.session_state.page_info = 'recommendation' + +def back_to_home_container(): + with st.container(border=True): + cols = st.columns([3,2,2]) + with cols[1]: + st.write('로그인이 필요합니다.') + cols = st.columns([4,2.5,3]) + with cols[1]: + st.link_button('메인페이지로 >>', st.session_state.url_main, type='primary') + +if not st.session_state.is_authenticated: + page_header() + back_to_home_container() + +else: + recommendation() diff --git a/frontend/recommendation.py b/frontend/recommendation.py new file mode 100644 index 0000000..5092663 --- /dev/null +++ b/frontend/recommendation.py @@ -0,0 +1,39 @@ +import streamlit as st + +def recommendation_page(): + + # 페이지 구성 + container = st.container(border=True) + + with container: + st.markdown("

이번 주 장바구니 만들기

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + st.markdown("
예산을 정해주세요.
", unsafe_allow_html=True) + + cols = st.columns([1,5,1]) + + if 'price' not in st.session_state: + st.session_state.price = 10000 + + def handle_change(): + st.session_state.price = st.session_state.price_slider + + with cols[1]: + + st.slider( + label='price', min_value=10000, max_value=1000000, value=50000, step=5000, + on_change=handle_change, key='price_slider' + ) + + cols = st.columns(5) + + with cols[2]: + st.write("예산: ", st.session_state.price, '원') + + + cols = st.columns(3) + + with cols[1]: + button2 = st.button("장바구니 추천받기", type="primary") + if button2: + st.session_state['page_info'] = 'result_page_1' From 208e61e06b3fb9dba565caafb16233af491b9c5a Mon Sep 17 00:00:00 2001 From: Judy Date: Sun, 17 Mar 2024 00:26:15 +0900 Subject: [PATCH 085/187] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=9B=84=20=EB=A9=94=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(=ED=98=84=EC=9E=AC=EB=8A=94=20=EB=8D=94?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EC=95=88=EB=90=A8)=20#43?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/main_2.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++ frontend/newapp.py | 3 +- 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 frontend/main_2.py diff --git a/frontend/main_2.py b/frontend/main_2.py new file mode 100644 index 0000000..d4f283d --- /dev/null +++ b/frontend/main_2.py @@ -0,0 +1,93 @@ +import streamlit as st +import requests + +from streamlit_extras.stylable_container import stylable_container + +def get_response(formatted_url): + response = requests.get(formatted_url) + if response.status_code == 200: + data = response.json() + else: + print(f'status code: {response.status_code}') + data = None + return data + +def get_my_recipe_data(): + + url = st.session_state.url_prefix + "/api/users/{user_id}/recipes/cooked?page_num={page_num}" + + recipe_list = [] + formatted_url = url.format(user_id=st.session_state.token['user_id'], page_num=1) + + while formatted_url: + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + + return recipe_list + +def get_my_recommended_data(): + + url = st.session_state.url_prefix + "/api/users/{user_id}/recipes/recommended?page_num={page_num}" + + recipe_list = [] + formatted_url = url.format(user_id=st.session_state.token['user_id'], page_num=1) + + while formatted_url: + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + + return recipe_list + +def display_my_recipe_container(my_recipe_list): + + container3_4 = st.container(border = True) + with container3_4: + st.markdown("

내가 해먹은 레시피

", unsafe_allow_html=True) + + cols = st.columns((1, 1, 1, 1, 1)) + + for i, my_recipe in enumerate(my_recipe_list): + with cols[i]: + with st.container(border=True): + st.image(my_recipe["recipe_img_url"]) + st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) + +def display_recommended_container(recommended_list): + + container3_4 = st.container(border = True) + + with container3_4: + st.markdown("

내가 좋아할 레시피

", unsafe_allow_html=True) + + cols = st.columns((1, 1, 1, 1, 1)) + + for i, recommended in enumerate(recommended_list): + with cols[i]: + with st.container(border=True): + st.image(recommended["recipe_img_url"]) + st.markdown(f"

{recommended['recipe_name']}

", unsafe_allow_html=True) + +def main_page_2(): + container3_1 = stylable_container( + key="container_with_border", + css_styles=""" + { + border: 1px solid rgba(49, 51, 63, 0.2); + border-radius: 0.5rem; + padding: calc(1em - 1px); + background-color: white; + } + """,) + with container3_1: + st.markdown("

나만의 식량 바구니에 \n 오신 것을 환영합니다!

", unsafe_allow_html=True) + st.markdown("

자신의 입맞에 맞는 레시피를 저장하고 \n 이번주에 구매할 식량 바구니를 추천받아보세요

", unsafe_allow_html=True) + + # recommended_preview + recommended_list = get_my_recommended_data() + display_recommended_container(recommended_list) + + # user_history_preview + my_recipe_list = get_my_recipe_data() + display_my_recipe_container(my_recipe_list) diff --git a/frontend/newapp.py b/frontend/newapp.py index 807aa35..11d291d 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -4,6 +4,7 @@ from basket_signup import signup_container from basket_login import login_container from main import main_page +from main_2 import main_page_2 from recommendation import recommendation_page def home(): @@ -12,7 +13,7 @@ def home(): def home2(): page_header() - st.write('login 됨') + main_page_2() def login(): page_header() From 8dd486b1dc4dce8446b7db504ebba81b4747bace Mon Sep 17 00:00:00 2001 From: Judy Date: Sun, 17 Mar 2024 00:59:41 +0900 Subject: [PATCH 086/187] =?UTF-8?q?feat:=20=EC=B6=94=EC=B2=9C=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/newapp.py | 9 ++- frontend/pages/recommendation.py | 7 ++ frontend/recommendation.py | 1 + frontend/result_page.py | 107 +++++++++++++++++++++++++++++++ 4 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 frontend/result_page.py diff --git a/frontend/newapp.py b/frontend/newapp.py index 11d291d..692744a 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -30,13 +30,12 @@ def recommendation(): if ('is_authenticated' not in st.session_state) and ('page_info' not in st.session_state): init() -value = st.session_state.get('key', 'default_value') -if (not st.session_state.get('is_authenticated', False)) and (st.session_state.get('page_info', '-') == 'home'): - home() -elif (not st.session_state.get('is_authenticated', False)) and (st.session_state.get('page_info', '-') == 'signup'): +if (not st.session_state.get('is_authenticated', False)) and (st.session_state.get('page_info', '-') == 'signup'): signup() elif (not st.session_state.get('is_authenticated', False)) and (st.session_state.get('page_info', '-') == 'login'): login() -elif st.session_state.get('is_authenticated', False) and (st.session_state.get('page_info', '-') == 'home2'): +elif not st.session_state.get('is_authenticated', False): + home() +elif st.session_state.get('is_authenticated', False): home2() diff --git a/frontend/pages/recommendation.py b/frontend/pages/recommendation.py index 625a0dc..344b958 100644 --- a/frontend/pages/recommendation.py +++ b/frontend/pages/recommendation.py @@ -2,6 +2,7 @@ from common import init, page_header from newapp import recommendation +from result_page import result_page from main import welcome_container #from recommendation import recommendation_page @@ -19,9 +20,15 @@ def back_to_home_container(): with cols[1]: st.link_button('메인페이지로 >>', st.session_state.url_main, type='primary') + +print(st.session_state.page_info) if not st.session_state.is_authenticated: page_header() back_to_home_container() +elif st.session_state.get('is_authenticated', False) and (st.session_state.get('page_info', '-') == 'result_page_1'): + result_page() + else: recommendation() + diff --git a/frontend/recommendation.py b/frontend/recommendation.py index 5092663..084be36 100644 --- a/frontend/recommendation.py +++ b/frontend/recommendation.py @@ -1,4 +1,5 @@ import streamlit as st +import requests def recommendation_page(): diff --git a/frontend/result_page.py b/frontend/result_page.py new file mode 100644 index 0000000..312a844 --- /dev/null +++ b/frontend/result_page.py @@ -0,0 +1,107 @@ +import math + +import streamlit as st +import requests + +def display_ingredients_in_rows_of_four(ingredients): + for ingredient in ingredients: + sub_container = st.container(border=True) + + with sub_container: + + cols = st.columns(5) + + with cols[0]: + st.markdown(f'Your Image', unsafe_allow_html=True) + + with cols[1]: + st.write(ingredient['ingredient_name']) + st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) + + with cols[-1]: + st.link_button('구매', ingredient['market_url'], type='primary') + +def display_recipes_in_rows_of_four(recipe_list, user_feedback=None): + + for row in range(math.ceil(len(recipe_list)/4)): + cols = st.columns(4) + + for i in range(4): + item_idx = i + row * 4 + if item_idx >= len(recipe_list): break + + item = recipe_list[item_idx] + with cols[i]: + with st.container(border=True): + st.markdown(f'Your Image', unsafe_allow_html=True) + + if user_feedback is None: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + else: + sub_cols = st.columns([3,1]) + with sub_cols[0]: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + with sub_cols[-1]: + show_feedback_button(item['recipe_id'], user_feedback) + +def basket_feedback(): + st.markdown("
방금 추천받은 장바구니 어땠나요?
", unsafe_allow_html=True) + st.text("") + cols = st.columns([2,1,1,2]) + with cols[1]: + st.button('좋아요') + with cols[2]: + st.button('싫어요') + +def post_recommendation(): + full_url = st.session_state.url_prefix + '/api/users/{user_id}/recommendations?price={price}' + formatted_url = full_url.format(user_id=st.session_state.token['user_id'], price=st.session_state.price) + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {st.session_state.token["token"]}', + } + + data = requests.post(formatted_url, headers=headers) + return data.json() +# response_json = response.json() if response.status_code == 200 else None +# +# return response.status_code, response_json + +def result_page(): + +# # 앱 헤더 +# page_header() + +# url = api_prefix + "users/{user_id}/previousrecommendation" +# formatted_url = url.format(user_id=st.session_state.user) +# data = get_response(formatted_url) + data = post_recommendation() + + # 페이지 구성 + container = st.container(border=True) + + with container: + + # 장바구니 추천 문구 + st.markdown("

새로운 장바구니를 추천받았어요!

", unsafe_allow_html=True) + st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) + + st.divider() + + # 구매할 식료품 목록 + st.markdown("

추천 장바구니

", unsafe_allow_html=True) + + display_ingredients_in_rows_of_four(data['ingredient_list']) + total_price = sum([ingredient['ingredient_price'] for ingredient in data['ingredient_list']]) + + st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) + + st.divider() + + # 이 장바구니로 만들 수 있는 음식 레시피 + st.markdown("

이 장바구니로 만들 수 있는 음식 레시피

", unsafe_allow_html=True) + display_recipes_in_rows_of_four(data['recipe_list']) + + st.text("\n\n") + basket_feedback() From 82437fb6e1d598d8598cf7bb0408c85d08b90e82 Mon Sep 17 00:00:00 2001 From: Judy Date: Sun, 17 Mar 2024 01:28:35 +0900 Subject: [PATCH 087/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=EB=B0=B1=20=ED=9E=88=EC=8A=A4=ED=86=A0=EB=A6=AC,=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=B6=94=EC=B2=9C=20=ED=9E=88=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#7=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/pages/recommendation_history.py | 11 ++++ frontend/pages/user_history.py | 11 ++++ frontend/recommendation_history.py | 39 ++++++++++++ frontend/user_history.py | 76 ++++++++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 frontend/pages/recommendation_history.py create mode 100644 frontend/pages/user_history.py create mode 100644 frontend/recommendation_history.py create mode 100644 frontend/user_history.py diff --git a/frontend/pages/recommendation_history.py b/frontend/pages/recommendation_history.py new file mode 100644 index 0000000..ff9ae5e --- /dev/null +++ b/frontend/pages/recommendation_history.py @@ -0,0 +1,11 @@ +import streamlit as st + +from common import init, page_header +from recommendation_history import recommendation_history_page +from pages.recommendation import back_to_home_container + +if not st.session_state.is_authenticated: + page_header() + back_to_home_container() +else: + recommendation_history_page() diff --git a/frontend/pages/user_history.py b/frontend/pages/user_history.py new file mode 100644 index 0000000..6c401e8 --- /dev/null +++ b/frontend/pages/user_history.py @@ -0,0 +1,11 @@ +import streamlit as st + +from common import init, page_header +from user_history import user_history_page +from pages.recommendation import back_to_home_container + +if not st.session_state.is_authenticated: + page_header() + back_to_home_container() +else: + user_history_page() diff --git a/frontend/recommendation_history.py b/frontend/recommendation_history.py new file mode 100644 index 0000000..77f504c --- /dev/null +++ b/frontend/recommendation_history.py @@ -0,0 +1,39 @@ +import streamlit as st + +from main_2 import get_response +from user_history import display_recipes_in_rows_of_four + +def get_and_stack_recipe_data_w_feedback(): + + url = st.session_state.url_prefix + "/api/users/{user_id}/recipes/recommended?page={page_num}" + recipe_list, user_feedback = [], [] + formatted_url = url.format(user_id=st.session_state.token['user_id'], page_num=1) + + while formatted_url: + print(formatted_url) + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + print(data['cooked_recipes_id']) + user_feedback = data['cooked_recipes_id'] + + return recipe_list, user_feedback + +def recommendation_history_page(): + + # get data + recipe_list, user_feedback = get_and_stack_recipe_data_w_feedback() + + # 페이지 구성 + container = st.container(border=True) + + with container: + + st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) + + sub_container = st.container(border=False) + with sub_container: + st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) + st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) + + display_recipes_in_rows_of_four(recipe_list, user_feedback) diff --git a/frontend/user_history.py b/frontend/user_history.py new file mode 100644 index 0000000..0206018 --- /dev/null +++ b/frontend/user_history.py @@ -0,0 +1,76 @@ +import math +import streamlit as st +import requests + +from main_2 import get_response + +def get_and_stack_recipe_data(): + + url = st.session_state.url_prefix + "/api/users/{user_id}/recipes/cooked?page={page_num}" + recipe_list = [] + formatted_url = url.format(user_id=st.session_state.token['user_id'], page_num=1) + + while formatted_url: + data = get_response(formatted_url) + recipe_list.extend(data['recipe_list']) + formatted_url = data['next_page_url'] + + return recipe_list + +def display_recipes_in_rows_of_four(recipe_list, user_feedback=None): + + for row in range(math.ceil(len(recipe_list)/4)): + cols = st.columns(4) + + for i in range(4): + item_idx = i + row * 4 + if item_idx >= len(recipe_list): break + + item = recipe_list[item_idx] + + with cols[i]: + with st.container(border=True): + st.markdown(f'Your Image', unsafe_allow_html=True) + + if user_feedback is None: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + else: + sub_cols = st.columns([3,1]) + with sub_cols[0]: + st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) + with sub_cols[-1]: + show_feedback_button(item['id'], user_feedback) + +def patch_feedback(user_id, recipe_id, current_state): + url = st.session_state.url_prefix + "/api/users/{user_id}/recipes/{recipe_id}/feedback" + data = { + 'feedback': not current_state + } + response = requests.patch(url.format(user_id=st.session_state.token['user_id'], recipe_id=recipe_id), json=data) + print(f'status code: {response.status_code}') + st.rerun() + +def show_feedback_button(recipe_id, user_feedback): + + icon_mapper = lambda cooked: '❤️' if cooked else '🩶' + cooked = recipe_id in user_feedback + + st.button( + icon_mapper(cooked), + on_click=patch_feedback, + key=f'{recipe_id}_feedback_button', + args=(st.session_state.token['user_id'], recipe_id, cooked)) + + +def user_history_page(): + + # get data + recipe_list = get_and_stack_recipe_data() + + # show container + container = st.container(border=True) + + with container: + # title + st.markdown("

❤️ 내가 요리한 레시피 ❤️

", unsafe_allow_html=True) + display_recipes_in_rows_of_four(recipe_list) From cb099f88d289178910a53c9412a86cf42b21f4d1 Mon Sep 17 00:00:00 2001 From: Judy Date: Sun, 17 Mar 2024 01:29:02 +0900 Subject: [PATCH 088/187] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=EC=A4=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/pages/recommendation.py | 2 -- frontend/recommendation.py | 1 - 2 files changed, 3 deletions(-) diff --git a/frontend/pages/recommendation.py b/frontend/pages/recommendation.py index 344b958..68ec7b4 100644 --- a/frontend/pages/recommendation.py +++ b/frontend/pages/recommendation.py @@ -20,8 +20,6 @@ def back_to_home_container(): with cols[1]: st.link_button('메인페이지로 >>', st.session_state.url_main, type='primary') - -print(st.session_state.page_info) if not st.session_state.is_authenticated: page_header() back_to_home_container() diff --git a/frontend/recommendation.py b/frontend/recommendation.py index 084be36..5092663 100644 --- a/frontend/recommendation.py +++ b/frontend/recommendation.py @@ -1,5 +1,4 @@ import streamlit as st -import requests def recommendation_page(): From 856e78f0d89e114e44e2205441d738a951af0c92 Mon Sep 17 00:00:00 2001 From: Judy Date: Mon, 18 Mar 2024 11:17:13 +0900 Subject: [PATCH 089/187] feat: front-end development dependency upload --- frontend/pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 frontend/pyproject.toml diff --git a/frontend/pyproject.toml b/frontend/pyproject.toml new file mode 100644 index 0000000..4356df4 --- /dev/null +++ b/frontend/pyproject.toml @@ -0,0 +1,7 @@ +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "pep440" +version = "0.1.0" +update_changelog_on_bump = true +major_version_zero = true From 6d372d19c6ebe77e4e24d481da984136f735ef4d Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 11:37:27 +0900 Subject: [PATCH 090/187] =?UTF-8?q?fix:=201=EC=B0=A8=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- .../recipes/repository/recipes_repository.py | 8 +- .../controller/request/signup_request.py | 27 ++--- .../controller/response/signup_response.py | 4 +- .../users/controller/user_controller.py | 102 ++++++++++++++++-- .../api/routes/users/service/user_service.py | 15 +-- 5 files changed, 124 insertions(+), 32 deletions(-) diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index fe18b66..a4d7d78 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -14,8 +14,8 @@ def __init__(self): self.ingredients_collection = data_source.collection_with_name_as("ingredients") - def select_user_by_user_id(self, user_id: str) -> User: - user = self.users_collection.find_one({"_id": ObjectId(user_id)}) + def select_user_by_user_id(self, login_id: str) -> User: + user = self.users_collection.find_one({"login_id": login_id}) if user: return User(**user) raise HTTPException(status_code=404, detail=f"User {id} not found") @@ -35,9 +35,9 @@ def select_ingredients_by_ingredients_id(self, ingredients_id: List[str]) -> Ing raise HTTPException(status_code=404, detail=f"Ingredients not found") - def update_cooked_recipes(self, user_id: str, user_cooked_recipes_id: List[str], user_recommended_recipes_id: List[str]) -> bool: + def update_cooked_recipes(self, login_id: str, user_cooked_recipes_id: List[str], user_recommended_recipes_id: List[str]) -> bool: update_result = self.users_collection.update_one( - {"_id": ObjectId(user_id)}, + {"login_id": login_id}, {"$set": { "feedback_history": user_cooked_recipes_id, "recommend_history_by_basket": user_recommended_recipes_id diff --git a/backend/app/api/routes/users/controller/request/signup_request.py b/backend/app/api/routes/users/controller/request/signup_request.py index eefd308..74bc182 100644 --- a/backend/app/api/routes/users/controller/request/signup_request.py +++ b/backend/app/api/routes/users/controller/request/signup_request.py @@ -1,8 +1,9 @@ import re +from fastapi import HTTPException from pydantic import BaseModel, validator -MINIMUM_LOGIN_ID_LENGTH = 5 +MINIMUM_LOGIN_ID_LENGTH = 3 MINIMUM_PASSWORD_LENGTH = 8 MINIMUM_FAVOR_RECIPE_COUNT = 10 @@ -15,31 +16,31 @@ class SignupRequest(BaseModel): @validator('login_id') def validate_login_id(cls, login_id: str): if not login_id.strip(): - raise ValueError(f"로그인 ID는 필수 입력입니다.") + raise HTTPException(status_code=400, detail=f"로그인 ID는 필수 입력입니다.") if len(login_id) < MINIMUM_LOGIN_ID_LENGTH: - raise ValueError(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + raise HTTPException(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") return login_id @validator('password') def validate_password(cls, password: str): if not password.strip(): - raise ValueError(f"비밀번호는 필수 입력입니다.") + raise HTTPException(status_code=400, detail=f"비밀번호는 필수 입력입니다.") if len(password) < MINIMUM_PASSWORD_LENGTH: - raise ValueError(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + raise HTTPException(status_code=400, detail=f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") return password @validator('nickname') def validate_nickname(cls, nickname: str): if not nickname.strip(): - raise ValueError(f"닉네임은 필수 입력입니다.") + raise HTTPException(status_code=400, detail=f"닉네임은 필수 입력입니다.") return nickname @validator('email') def validate_email(cls, email: str): if not email.strip(): - raise ValueError(f"이메일은 필수 입력입니다.") + raise HTTPException(status_code=400, detail=f"이메일은 필수 입력입니다.") if not cls._valid_email(email): - raise ValueError(f"이메일 형식에 맞지 않습니다: {email}") + raise HTTPException(status_code=400, detail=f"이메일 형식에 맞지 않습니다: {email}") return email @staticmethod @@ -55,17 +56,17 @@ class LoginRequest(BaseModel): @validator('login_id') def validate_login_id(cls, login_id: str): if not login_id.strip(): - raise ValueError(f"로그인 ID는 필수 입력입니다.") + raise HTTPException(status_code=400, detail=f"로그인 ID는 필수 입력입니다.") if len(login_id) < MINIMUM_LOGIN_ID_LENGTH: - raise ValueError(f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") + raise HTTPException(status_code=400, detail=f"로그인 ID는 최소 {MINIMUM_LOGIN_ID_LENGTH} 자리 이상이어야 합니다: {len(login_id)}") return login_id @validator('password') def validate_password(cls, password: str): if not password.strip(): - raise ValueError(f"비밀번호는 필수 입력입니다.") + raise HTTPException(status_code=400, detail=f"비밀번호는 필수 입력입니다.") if len(password) < MINIMUM_PASSWORD_LENGTH: - raise ValueError(f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") + raise HTTPException(status_code=400, detail=f"비밀번호는 최소 {MINIMUM_PASSWORD_LENGTH} 자리 이상이어야 합니다: {len(password)}") return password @@ -75,5 +76,5 @@ class UserFavorRecipesRequest(BaseModel): @validator('recipes') def validate_login_id(cls, recipes: list[str]): if len(set(recipes)) < MINIMUM_FAVOR_RECIPE_COUNT: - raise ValueError(f"좋아하는 레시피는 최소 {MINIMUM_FAVOR_RECIPE_COUNT} 개 이상이어야 합니다: {len(set(recipes))}") + raise HTTPException(status_code=400, detail=f"좋아하는 레시피는 최소 {MINIMUM_FAVOR_RECIPE_COUNT} 개 이상이어야 합니다: {len(set(recipes))}") return recipes diff --git a/backend/app/api/routes/users/controller/response/signup_response.py b/backend/app/api/routes/users/controller/response/signup_response.py index 3b769bd..fc52c23 100644 --- a/backend/app/api/routes/users/controller/response/signup_response.py +++ b/backend/app/api/routes/users/controller/response/signup_response.py @@ -8,7 +8,9 @@ class SignupResponse(BaseModel): email: str class LoginResponse(BaseModel): - _id: str + token: str + login_id: str + password: str class FavorRecipesResponse(BaseModel): recipes: list \ No newline at end of file diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 8931473..cc8e717 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -9,14 +9,18 @@ from .request.signup_request import SignupRequest, LoginRequest, UserFavorRecipesRequest from .response.signup_response import SignupResponse, LoginResponse, FavorRecipesResponse from ..service.user_service import UserService +from api.routes.recipes.service.recipes_service import RecipesService from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository, RecommendationRepository from ..dto.user_dto import UserSignupDTO, UserLoginDTO class UserController: - def __init__(self, user_service: UserService): + def __init__(self, user_service: UserService, recipe_service: RecipesService): self.user_service: UserService = user_service + self.recipe_service: RecipesService = recipe_service async def sign_up(self, signup_request: SignupRequest) -> SignupResponse: + self.user_service.is_login_id_usable(signup_request.login_id) + self.user_service.is_nickname_usable(signup_request.nickname) return SignupResponse( **dict(self.user_service.sign_up( UserSignupDTO(**dict(signup_request)) @@ -45,17 +49,23 @@ async def recommended_basket(self, user_id: str, price: int): top_k_recipes = self.user_service.top_k_recipes(user_id, price) # recipe 정보 가져오기 - recipe_infos = {} # self.recipe_service.infos(top_k_recipes) + recipe_infos = self.recipe_service.get_recipes_by_recipes_id(top_k_recipes) # ingredient 정보 가져오기 - ingredient_infos = {} # self.ingredient_service.infos(recipe_infos) + price_infos = self.recipe_service.get_prices_by_ingredients_id(recipe_infos['']) basket_info = self.user_service.recommended_basket(recipe_infos) return -user_controller = UserController(UserService( - UserRepository(), SessionRepository(), FoodRepository(), RecommendationRepository())) +user_controller = UserController( + UserService( + UserRepository(), + SessionRepository(), + FoodRepository(), + RecommendationRepository()), + RecipesService() +) user_router = APIRouter() class Request(BaseModel): @@ -74,7 +84,7 @@ async def sign_up(request: SignupRequest) -> JSONResponse: email=request.email)) return JSONResponse(content=response_body.model_dump(), status_code=status.HTTP_200_OK) -@user_router.post('/api/users/auth') +@user_router.post('/api/users/auths') async def login(request: LoginRequest) -> Response: # logging.info(request) response_body = await user_controller.login(UserLoginDTO( @@ -110,5 +120,83 @@ async def save_favor_recipes(user_id: str, request: UserFavorRecipesRequest) -> @user_router.post('/api/users/{user_id}/recommendations') async def get_recommendation(user_id: str, price: int) -> JSONResponse: - response_body = {}# await user_controller.recommended_basket(user_id, price) + response_body = { + "basket_price": 46000, + "ingredient_list": [ + { + "ingredient_id": 10, + "ingredient_name": "브로콜리", + "ingredient_amount": 1, + "ingredient_unit": "kg", + "ingredient_price": 4680, + "img_link": "https://health.chosun.com/site/data/img_dir/2024/01/19/2024011902009_0.jpg", + "market_url": + "https://www.coupang.com/vp/products/4874444452?itemId=6339533080&vendorItemId=73634892616&pickType=COU_PICK&q=%EB%B8%8C%EB%A1%9C%EC%BD%9C%EB%A6%AC&itemsCount=36&searchId=891d0b69dc8f452daf392e3db2482732&rank=1&isAddedCart=" + }, + { + "ingredient_id": 11, + "ingredient_name": "초고추장", + "ingredient_amount": 500, + "ingredient_unit": "g", + "ingredient_price": 5000, + "img_link": + "https://image7.coupangcdn.com/image/retail/images/4810991441045098-31358d86-eff6-45f4-8ed6-f36b642e8944.jpg", + "market_url": + "https://www.coupang.com/vp/products/6974484284?itemId=17019959259&vendorItemId=3000138402&q=%EC%B4%88%EA%B3%A0%EC%B6%94%EC%9E%A5&itemsCount=36&searchId=d5538b6e86d04be3938c98ef1655df85&rank=1&isAddedCart=" + } + ], + "recipe_list": [ + { + "recipe_id": 1, + "recipe_name": "어묵 김말이", + "ingredient": [ + {"ingredient_id": "1", + "ingredient_name": "어묵"}, + {"ingredient_id": "2", + "ingredient_name": "김말이"} + ], + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg", + "recipe_url": "https://www.10000recipe.com/recipe/128671" + }, + { + "recipe_id": 2, + "recipe_name": "두부새우전", + "ingredient": [ + {"ingredient_id": "3", + "ingredient_name": "두부"}, + {"ingredient_id": "4", + "ingredient_name": "새우"} + ], + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg", + "recipe_url": "https://www.10000recipe.com/recipe/128932" + }, + { + "recipe_id": 3, + "recipe_name": "알밥", + "ingredient": [ + {"ingredient_id": "5", + "ingredient_name": "밥"}, + {"ingredient_id": "6", + "ingredient_name": "날치알"} + ], + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg", + "recipe_url": "https://www.10000recipe.com/recipe/131871" + }, + { + "recipe_id": 4, + "recipe_name": "현미호두죽", + "ingredient": [ + {"ingredient_id": "5", + "ingredient_name": "밥"}, + {"ingredient_id": "7", + "ingredient_name": "현미"}, + {"ingredient_id": "8", + "ingredient_name": "호두"} + ], + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg", + "recipe_url": "https://www.10000recipe.com/recipe/128671" + } + ] + } + response_body = await user_controller.recommended_basket(user_id, price) return JSONResponse(content=response_body, status_code=status.HTTP_200_OK) diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index 313db1a..cc7d06f 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -1,6 +1,7 @@ import uuid import pulp +from fastapi import HTTPException from datetime import datetime, timedelta from ..entity.user import User @@ -28,7 +29,7 @@ def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: def login(self, login_request: UserLoginDTO) -> UserLoginDTO: user = self.user_repository.find_one({'login_id': login_request.login_id, 'password': login_request.password}) if user is None: - raise ValueError("아이디와 비밀번호가 일치하지 않습니다.") + raise HTTPException(status_code=400, detail="아이디와 비밀번호가 일치하지 않습니다.") user = User(**dict(user)) token = str(uuid.uuid4()) @@ -37,12 +38,12 @@ def login(self, login_request: UserLoginDTO) -> UserLoginDTO: def is_login_id_usable(self, login_id: str) -> bool: if self.user_repository.find_one({'login_id': login_id}) is not None: - raise ValueError(f"중복되는 아이디 입니다: {login_id}") + raise HTTPException(status_code=409, detail=f"중복되는 아이디 입니다: {login_id}") return True def is_nickname_usable(self, nickname: str) -> bool: if self.user_repository.find_one({'nickname': nickname}) is not None: - raise ValueError(f"중복되는 닉네임 입니다: {nickname}") + raise HTTPException(status_code=409, detail=f"중복되는 닉네임 입니다: {nickname}") return True def favor_recipes(self, page_num: int) -> list: @@ -53,14 +54,14 @@ def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> def top_k_recipes(self, login_id: str, price: int) -> list: # user에 inference된 recipes - return self.recommendation_repository.find({'login_id': login_id}) + return self.recommendation_repository.find_by_login_id(login_id) - def recommended_basket(self, recipe_infos: dict, price: int) -> dict: + def recommended_basket(self, recipe_infos: dict, price_infos: dict, price: int) -> dict: # 입력값 파싱 - recipe_requirement_infos, price_infos = self._parse(recipe_infos) + # recipe_requirement_infos, price_infos = self._parse(recipe_infos) # 이진 정수 프로그래밍 - return self._optimized_results(recipe_requirement_infos, price_infos, price) + return self._optimized_results(recipe_infos, price_infos, price) def _parse(self, recipe_infos: dict): recipe_requirement_infos, price_infos = None, None From 771d62c678893ec148a3806aad35b1a617ed70c2 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 12:24:25 +0900 Subject: [PATCH 091/187] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=A9=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- backend/app/api/routes/recipes/entity/ingredient.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/app/api/routes/recipes/entity/ingredient.py b/backend/app/api/routes/recipes/entity/ingredient.py index 66922bf..50241f0 100644 --- a/backend/app/api/routes/recipes/entity/ingredient.py +++ b/backend/app/api/routes/recipes/entity/ingredient.py @@ -26,3 +26,6 @@ def get_id(self): def get_name(self): return self.name + + def get_price(self): + return self.price From 2eb479514bbdf8fec399db282e714708b0a6d8ec Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 12:25:16 +0900 Subject: [PATCH 092/187] =?UTF-8?q?feat:=20=EC=9E=AC=EB=A3=8C=20ID=20set?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- backend/app/api/routes/recipes/entity/recipes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/entity/recipes.py b/backend/app/api/routes/recipes/entity/recipes.py index 5909118..5f94453 100644 --- a/backend/app/api/routes/recipes/entity/recipes.py +++ b/backend/app/api/routes/recipes/entity/recipes.py @@ -8,4 +8,6 @@ class Recipes(BaseModel): def get_recipes(self): return self.recipes - \ No newline at end of file + + def get_ingredients(self): + return set.union(*(set(recipe.get_ingredients()) for recipe in self.recipes)) \ No newline at end of file From b8e228efcceb99343c81fe5583536ab1ad87369b Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 12:27:16 +0900 Subject: [PATCH 093/187] =?UTF-8?q?feat:=20=EC=9E=AC=EB=A3=8C=20=EA=B0=80?= =?UTF-8?q?=EA=B2=A9=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- .../app/api/routes/recipes/service/recipes_service.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py index 56936e9..b77b6a1 100644 --- a/backend/app/api/routes/recipes/service/recipes_service.py +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -1,4 +1,5 @@ from typing import List + from ..repository.recipes_repository import RecipesRepository from ..entity.recipes import Recipes from ..entity.ingredients import Ingredients @@ -9,7 +10,6 @@ class RecipesService: def __init__(self, recipes_repository: RecipesRepository = None): self.recipes_repository = RecipesRepository() - def get_user_by_user_id(self, user_id: str) -> User: user = self.recipes_repository.select_user_by_user_id(user_id) return user @@ -27,8 +27,7 @@ def get_user_recommended_recipes_id_by_user(self, user: User) -> List[str]: def get_recipes_by_recipes_id(self, recipes_id: List[str]) -> Recipes: # 레시피 리스트 조회 - recipes = self.recipes_repository.select_recipes_by_recipes_id(recipes_id) - return recipes + return self.recipes_repository.select_recipes_by_recipes_id(recipes_id) def get_ingredients_list_by_recipes(self, recipes: Recipes) -> List[Ingredients]: @@ -52,4 +51,7 @@ def update_cooked_recipes(self, user_id: str, recipes_id: str, feedback: bool): user_cooked_recipes_id.remove(recipes_id) user_recommended_recipes_id.append(recipes_id) return self.recipes_repository.update_cooked_recipes(user_id, user_cooked_recipes_id, user_recommended_recipes_id) - \ No newline at end of file + + def get_prices_by_ingredients_id(self, ingredients_id: List[str]): + ingredients = self.recipes_repository.select_ingredients_by_ingredients_id(ingredients_id).get_ingredients() + return {ingredient.get_id(): ingredient.get_price() for ingredient in ingredients} \ No newline at end of file From f4e3f66fabb28f2dc8a5e8dd32a672df01801348 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 12:29:19 +0900 Subject: [PATCH 094/187] =?UTF-8?q?feat:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=B6=94=EC=B2=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- .../users/repository/user_repository.py | 7 ++-- .../api/routes/users/service/user_service.py | 33 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index c147139..cefaeb3 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -64,5 +64,8 @@ def __init__(self): self.collection = data_source.collection_with_name_as('model_recommendation_histories') def find_by_login_id(self, login_id: str) -> list: - result = self.collection.find_one({'login_id': login_id}) - return result['recipe_top_20'] + result = self.collection.find_one({'id': login_id}) + return result['recommended_item'] + + def save(self, recommendation): + self.collection.insert_one(recommendation) diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index cc7d06f..e4b0c7e 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -62,10 +62,15 @@ def recommended_basket(self, recipe_infos: dict, price_infos: dict, price: int) # 이진 정수 프로그래밍 return self._optimized_results(recipe_infos, price_infos, price) - - def _parse(self, recipe_infos: dict): - recipe_requirement_infos, price_infos = None, None - return recipe_requirement_infos, price_infos + + def save_basket(self, user_id, price, datetime, recommended_basket) -> None: + self.recommendation_repository.save({ + 'user_id': user_id, + 'price': price, + 'datetime': datetime, + 'ingredients': recommended_basket['ingredient_list'], + 'recipes': recommended_basket['recipe_list'], + 'basket_price': 0}) def _optimized_results(self, recipe_requirement_infos: dict, @@ -78,10 +83,6 @@ def _optimized_results(self, # 문제 인스턴스 생성 prob = pulp.LpProblem("MaximizeNumberOfDishes", pulp.LpMaximize) - # ingredients_info = { - # 'A': {'price': 70, 'amount': 5}, - # 'B': {'price': 30, 'amount': 2}, - # 'C': {'price': 20, 'amount': 2}} # ingredients_price = {'신김치': 7000, '돼지고기': 5000, '양파': 3000, '두부': 1500, '애호박': 2000, '청양고추': 1000, '계란': 6000, '밀가루': 3000} # 식재료 가격 및 판매단위 변수 (0 또는 1의 값을 가짐) x = pulp.LpVariable.dicts("ingredient", price_infos.keys(), cat='Binary') # 재료 포함 여부 @@ -97,25 +98,25 @@ def _optimized_results(self, # requirements = {'돼지고기김치찌개': ['신김치', '돼지고기', '양파', '두부'], '된장찌개': ['두부', '애호박', '청양고추', '양파'], '애호박전': ['애호박', '계란', '밀가루']} # 식재료 제한: 각 요리를 최대한 살 수 있는 한도 - a = {ingredient: (MAX_PRICE//info['price'])*info['amount'] \ - for ingredient, info in price_infos.items()} - print(a) + # a = {ingredient: (MAX_PRICE//info['price'])*info['amount'] \ + # for ingredient, info in price_infos.items()} + # print(a) # 목표 함수 (최대화하고자 하는 요리의 수) prob += pulp.lpSum([y[dish] for dish in dishes]) # 비용 제약 조건 - prob += pulp.lpSum([price_infos[i]['price']*x[i] for i in price_infos]) <= MAX_PRICE + prob += pulp.lpSum([price_infos[i]*x[i] for i in price_infos]) <= MAX_PRICE # 요리별 식재료 제약 조건 for dish in dishes: - for ingredient in recipe_requirement_infos[dish].keys(): + for ingredient in recipe_requirement_infos[dish]: prob += (x[ingredient] >= y[dish]) # 정량 제약 조건: 요리에 사용되는 재료의 총합은 각 재료의 상한을 넘지 못함 - for ingredient in price_infos.keys(): - total_amount = pulp.lpSum([y[dish] * requirement[ingredient] for dish, requirement in recipe_requirement_infos.items() if ingredient in requirement]) - prob += (total_amount <= a[ingredient] * x[ingredient]) + # for ingredient in price_infos.keys(): + # total_amount = pulp.lpSum([y[dish] * requirement[ingredient] for dish, requirement in recipe_requirement_infos.items() if ingredient in requirement]) + # prob += (total_amount <= a[ingredient] * x[ingredient]) # 문제 풀기 prob.solve() From b8225dbc26e73d78ae7332321aec1b87047e00ee Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 13:12:51 +0900 Subject: [PATCH 095/187] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9E=AC=EB=A3=8C=20=EC=B6=9C=EB=A0=A5=20=ED=8F=BC?= =?UTF-8?q?=20=EB=B3=80=ED=99=98=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- .../api/routes/recipes/entity/ingredient.py | 22 ++++++++++++++++++ .../app/api/routes/recipes/entity/recipe.py | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/backend/app/api/routes/recipes/entity/ingredient.py b/backend/app/api/routes/recipes/entity/ingredient.py index 50241f0..18b48b8 100644 --- a/backend/app/api/routes/recipes/entity/ingredient.py +++ b/backend/app/api/routes/recipes/entity/ingredient.py @@ -29,3 +29,25 @@ def get_name(self): def get_price(self): return self.price + + def as_basket_form(self): + '''{ + "ingredient_id": 10, + "ingredient_name": "브로콜리", + "ingredient_amount": 1, + "ingredient_unit": "kg", + "ingredient_price": "4680", + "img_link": "https://health.chosun.com/site/data/img_dir/2024/01/19/2024011902009_0.jpg", + "market_url": + "https://www.coupang.com/vp/products/4874444452?itemId=6339533080&vendorItemId=73634892616&pickType=COU_PICK&q=%EB%B8%8C%EB%A1%9C%EC%BD%9C%EB%A6%AC&itemsCount=36&searchId=891d0b69dc8f452daf392e3db2482732&rank=1&isAddedCart=" + }''' + + return { + 'ingredient_id': self.id, + 'ingredient_name': self.name, + 'ingredient_amount': 0, + 'ingredient_unit': 'None', + 'ingredient_price': self.price, + 'img_link': 'None', + 'market_url': self.price_url, + } diff --git a/backend/app/api/routes/recipes/entity/recipe.py b/backend/app/api/routes/recipes/entity/recipe.py index 4b97345..b9ab3fe 100644 --- a/backend/app/api/routes/recipes/entity/recipe.py +++ b/backend/app/api/routes/recipes/entity/recipe.py @@ -50,3 +50,26 @@ def get_recipe_img_url(self): def get_ingredients(self): return self.ingredient + + + def as_basket_form(self): + '''{ + "recipe_id": 1, + "recipe_name": "어묵 김말이", + "ingredient": [ + {"ingredient_id": "1", + "ingredient_name": "어묵"}, + {"ingredient_id": "2", + "ingredient_name": "김말이"} + ], + "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg", + "recipe_url": "https://www.10000recipe.com/recipe/128671" + } + ''' + return { + 'recipe_id': self.id, + 'recipe_name': self.recipe_name, + 'ingredient': self.ingredient, + 'recipe_img_url': self.recipe_img_url, + 'recipe_url': self.recipe_url, + } From 5d685d76a8d1fc43c3d6a642384ec698c52c0ccb Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 13:13:27 +0900 Subject: [PATCH 096/187] =?UTF-8?q?refactor:=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=20=EB=82=B4=20=EC=A0=84=EC=B2=B4=20=EC=9E=AC=EB=A3=8C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- backend/app/api/routes/recipes/entity/recipes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/entity/recipes.py b/backend/app/api/routes/recipes/entity/recipes.py index 5f94453..3b368f6 100644 --- a/backend/app/api/routes/recipes/entity/recipes.py +++ b/backend/app/api/routes/recipes/entity/recipes.py @@ -9,5 +9,5 @@ class Recipes(BaseModel): def get_recipes(self): return self.recipes - def get_ingredients(self): + def get_total_ingredients_set(self): return set.union(*(set(recipe.get_ingredients()) for recipe in self.recipes)) \ No newline at end of file From 00cde6297141a36241c237198dd2d562231803e4 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 13:14:31 +0900 Subject: [PATCH 097/187] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=82=B4=20=EC=9E=AC=EB=A3=8C=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- backend/app/api/routes/recipes/service/recipes_service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/recipes/service/recipes_service.py b/backend/app/api/routes/recipes/service/recipes_service.py index b77b6a1..4d791b4 100644 --- a/backend/app/api/routes/recipes/service/recipes_service.py +++ b/backend/app/api/routes/recipes/service/recipes_service.py @@ -54,4 +54,7 @@ def update_cooked_recipes(self, user_id: str, recipes_id: str, feedback: bool): def get_prices_by_ingredients_id(self, ingredients_id: List[str]): ingredients = self.recipes_repository.select_ingredients_by_ingredients_id(ingredients_id).get_ingredients() - return {ingredient.get_id(): ingredient.get_price() for ingredient in ingredients} \ No newline at end of file + return {ingredient.get_id(): ingredient.get_price() for ingredient in ingredients} + + def get_ingredients_by_ingredients_id(self, ingredients_id: List[str]): + return self.recipes_repository.select_ingredients_by_ingredients_id(ingredients_id).get_ingredients() From 0a152bb17fcbd2b0639d58a9d234416bfa5a995b Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 13:17:17 +0900 Subject: [PATCH 098/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=82=B4=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- .../users/controller/user_controller.py | 95 ++++++++----------- 1 file changed, 38 insertions(+), 57 deletions(-) diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index cc8e717..445e163 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -1,11 +1,13 @@ -from fastapi import APIRouter, Query, Request, Response, status +from fastapi import APIRouter, Query, Response, status from typing import Optional +from datetime import datetime as dt import logging from fastapi.responses import JSONResponse from pydantic import BaseModel +from api.routes.recipes.entity.recipes import Recipes from .request.signup_request import SignupRequest, LoginRequest, UserFavorRecipesRequest from .response.signup_response import SignupResponse, LoginResponse, FavorRecipesResponse from ..service.user_service import UserService @@ -52,10 +54,38 @@ async def recommended_basket(self, user_id: str, price: int): recipe_infos = self.recipe_service.get_recipes_by_recipes_id(top_k_recipes) # ingredient 정보 가져오기 - price_infos = self.recipe_service.get_prices_by_ingredients_id(recipe_infos['']) + price_infos = self.recipe_service.get_prices_by_ingredients_id(recipe_infos.get_total_ingredients_set()) - basket_info = self.user_service.recommended_basket(recipe_infos) - return + # 장바구니 추천 + recipes = recipe_infos.get_recipes() + recipes = {recipe.get_id(): recipe.get_ingredients() for recipe in recipes} + + recommended_basket = self.user_service.recommended_basket(recipes, price_infos, price) + + # 추천 장바구니 결과 저장 + self.user_service.save_basket(user_id, price, dt.now(), recommended_basket) + + recommended_basket = self._basket_with_infos(recommended_basket, recipe_infos) + + return recommended_basket + + def _basket_with_infos(self, recommended_basket: dict, recipe_infos: Recipes): + logging.debug(recommended_basket) + # logging.debug(recipe_infos) + + recipe_info_list = [recipe.as_basket_form() for recipe in recipe_infos.get_recipes() if recipe.get_id() in recommended_basket['recipe_list']] + # logging.debug('Basket Form', recipe_info_list) + + total_ingredients = self.recipe_service.get_ingredients_by_ingredients_id(recipe_infos.get_total_ingredients_set()) + ingredient_info_list = [ingredient.as_basket_form() for ingredient in total_ingredients if ingredient.get_id() in recommended_basket['ingredient_list']] + # logging.debug('Ingredient Basket Form', ingredient_info_list) + + basket_with_infos = { + 'basket_price': 0, + 'ingredient_list': ingredient_info_list, + 'recipe_list': recipe_info_list, + } + return basket_with_infos user_controller = UserController( @@ -120,7 +150,7 @@ async def save_favor_recipes(user_id: str, request: UserFavorRecipesRequest) -> @user_router.post('/api/users/{user_id}/recommendations') async def get_recommendation(user_id: str, price: int) -> JSONResponse: - response_body = { + '''{ "basket_price": 46000, "ingredient_list": [ { @@ -132,18 +162,7 @@ async def get_recommendation(user_id: str, price: int) -> JSONResponse: "img_link": "https://health.chosun.com/site/data/img_dir/2024/01/19/2024011902009_0.jpg", "market_url": "https://www.coupang.com/vp/products/4874444452?itemId=6339533080&vendorItemId=73634892616&pickType=COU_PICK&q=%EB%B8%8C%EB%A1%9C%EC%BD%9C%EB%A6%AC&itemsCount=36&searchId=891d0b69dc8f452daf392e3db2482732&rank=1&isAddedCart=" - }, - { - "ingredient_id": 11, - "ingredient_name": "초고추장", - "ingredient_amount": 500, - "ingredient_unit": "g", - "ingredient_price": 5000, - "img_link": - "https://image7.coupangcdn.com/image/retail/images/4810991441045098-31358d86-eff6-45f4-8ed6-f36b642e8944.jpg", - "market_url": - "https://www.coupang.com/vp/products/6974484284?itemId=17019959259&vendorItemId=3000138402&q=%EC%B4%88%EA%B3%A0%EC%B6%94%EC%9E%A5&itemsCount=36&searchId=d5538b6e86d04be3938c98ef1655df85&rank=1&isAddedCart=" - } + },... ], "recipe_list": [ { @@ -157,46 +176,8 @@ async def get_recommendation(user_id: str, price: int) -> JSONResponse: ], "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2015/05/18/1fb83f8578488ba482ad400e3b62df49.jpg", "recipe_url": "https://www.10000recipe.com/recipe/128671" - }, - { - "recipe_id": 2, - "recipe_name": "두부새우전", - "ingredient": [ - {"ingredient_id": "3", - "ingredient_name": "두부"}, - {"ingredient_id": "4", - "ingredient_name": "새우"} - ], - "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/8d7a003794ac7ab77e5777796d9c20dd.jpg", - "recipe_url": "https://www.10000recipe.com/recipe/128932" - }, - { - "recipe_id": 3, - "recipe_name": "알밥", - "ingredient": [ - {"ingredient_id": "5", - "ingredient_name": "밥"}, - {"ingredient_id": "6", - "ingredient_name": "날치알"} - ], - "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2015/06/09/54d80fba5f2615d0a6bbd960adf4296c.jpg", - "recipe_url": "https://www.10000recipe.com/recipe/131871" - }, - { - "recipe_id": 4, - "recipe_name": "현미호두죽", - "ingredient": [ - {"ingredient_id": "5", - "ingredient_name": "밥"}, - {"ingredient_id": "7", - "ingredient_name": "현미"}, - {"ingredient_id": "8", - "ingredient_name": "호두"} - ], - "recipe_img_url": "https://recipe1.ezmember.co.kr/cache/recipe/2017/07/19/993a1efe45598cf296076874df509bfe1.jpg", - "recipe_url": "https://www.10000recipe.com/recipe/128671" - } + },... ] - } + }''' response_body = await user_controller.recommended_basket(user_id, price) return JSONResponse(content=response_body, status_code=status.HTTP_200_OK) From 9be1fb1453e0ac38d70b4770c8e797c4522de07d Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 14:08:43 +0900 Subject: [PATCH 099/187] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=93=88=20import?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- .../routes/recipes/dto/get_recipes_reponse.py | 2 +- .../api/routes/recipes/entity/ingredient.py | 2 +- .../app/api/routes/recipes/entity/recipe.py | 2 +- backend/app/api/routes/recipes/entity/user.py | 2 +- .../recipes/repository/recipes_repository.py | 2 +- .../users/controller/user_controller.py | 4 +- .../users/repository/user_repository.py | 2 +- backend/app/app.py | 4 +- backend/app/database/data_source.py | 2 +- .../app/exception/users/signup_exeption.py | 14 ------- backend/conftest.py | 10 +++++ backend/poetry.lock | 40 ++++++++++++++++++- backend/pyproject.toml | 1 + .../users/controller/test_user_controller.py | 2 +- backend/tests/database/test_database.py | 4 +- 15 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 backend/conftest.py diff --git a/backend/app/api/routes/recipes/dto/get_recipes_reponse.py b/backend/app/api/routes/recipes/dto/get_recipes_reponse.py index 07e8872..e0b1576 100644 --- a/backend/app/api/routes/recipes/dto/get_recipes_reponse.py +++ b/backend/app/api/routes/recipes/dto/get_recipes_reponse.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from utils.pyobject_id import PyObjectId +from app.utils.pyobject_id import PyObjectId from typing import Dict diff --git a/backend/app/api/routes/recipes/entity/ingredient.py b/backend/app/api/routes/recipes/entity/ingredient.py index 18b48b8..dd60410 100644 --- a/backend/app/api/routes/recipes/entity/ingredient.py +++ b/backend/app/api/routes/recipes/entity/ingredient.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field, ConfigDict -from utils.pyobject_id import PyObjectId +from app.utils.pyobject_id import PyObjectId class Ingredient(BaseModel): diff --git a/backend/app/api/routes/recipes/entity/recipe.py b/backend/app/api/routes/recipes/entity/recipe.py index b9ab3fe..c090c1e 100644 --- a/backend/app/api/routes/recipes/entity/recipe.py +++ b/backend/app/api/routes/recipes/entity/recipe.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, Field, ConfigDict -from utils.pyobject_id import PyObjectId +from app.utils.pyobject_id import PyObjectId from typing import List diff --git a/backend/app/api/routes/recipes/entity/user.py b/backend/app/api/routes/recipes/entity/user.py index 29d6939..d7f9635 100644 --- a/backend/app/api/routes/recipes/entity/user.py +++ b/backend/app/api/routes/recipes/entity/user.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, Field, ConfigDict from typing import List -from utils.pyobject_id import PyObjectId +from app.utils.pyobject_id import PyObjectId class User(BaseModel): diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index a4d7d78..591cf77 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -1,7 +1,7 @@ from fastapi import HTTPException from bson import ObjectId from typing import List -from database.data_source import data_source +from app.database.data_source import data_source from ..entity.user import User from ..entity.recipes import Recipes from ..entity.ingredients import Ingredients diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 445e163..b2c2d86 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -7,11 +7,11 @@ from pydantic import BaseModel -from api.routes.recipes.entity.recipes import Recipes +from app.api.routes.recipes.entity.recipes import Recipes from .request.signup_request import SignupRequest, LoginRequest, UserFavorRecipesRequest from .response.signup_response import SignupResponse, LoginResponse, FavorRecipesResponse from ..service.user_service import UserService -from api.routes.recipes.service.recipes_service import RecipesService +from app.api.routes.recipes.service.recipes_service import RecipesService from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository, RecommendationRepository from ..dto.user_dto import UserSignupDTO, UserLoginDTO diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index cefaeb3..61ae266 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -1,5 +1,5 @@ from datetime import datetime -from database.data_source import data_source +from app.database.data_source import data_source from ..dto.user_dto import UserSignupDTO, UserLoginDTO import logging import pymongo diff --git a/backend/app/app.py b/backend/app/app.py index 6e0bedf..aed9e21 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -1,7 +1,7 @@ import uvicorn from fastapi import FastAPI, APIRouter -from api.routes.users.controller.user_controller import user_router -from api.routes.recipes.controller.recipes_controller import recipes_router +from app.api.routes.users.controller.user_controller import user_router +from app.api.routes.recipes.controller.recipes_controller import recipes_router def new_app() -> FastAPI: return FastAPI() diff --git a/backend/app/database/data_source.py b/backend/app/database/data_source.py index c6808e1..cc538c6 100644 --- a/backend/app/database/data_source.py +++ b/backend/app/database/data_source.py @@ -7,7 +7,7 @@ from pydantic import BaseModel from typing import Optional -from exception.database.database_exception import ( +from app.exception.database.database_exception import ( DatabaseNotFoundException, CollectionNotFoundException ) diff --git a/backend/app/exception/users/signup_exeption.py b/backend/app/exception/users/signup_exeption.py index a991dec..05f0aea 100644 --- a/backend/app/exception/users/signup_exeption.py +++ b/backend/app/exception/users/signup_exeption.py @@ -1,19 +1,5 @@ -from fastapi import status, Request -from fastapi.responses import JSONResponse - -from ...app import app from .user_exception import UserException -@app.exception_handler(UserException) -async def custom_exception_handler(request : Request, exc: UserException): - if isinstance(exc, UserSignUpLoginIdMissningException): - status_code = status.HTTP_400_BAD_REQUEST - content = {'message': exc.message} - return JSONResponse( - status_code=status_code, # 예외에 따라 상태 코드를 조정할 수 있습니다. - content=content - ) - class UserSingUpException(UserException): def __init__(self, message): super().__init__(message) diff --git a/backend/conftest.py b/backend/conftest.py new file mode 100644 index 0000000..184f3cb --- /dev/null +++ b/backend/conftest.py @@ -0,0 +1,10 @@ +# backend/conftest.py +import os +import sys + +# 현재 파일의 경로를 구함 +current_dir = os.path.dirname(os.path.abspath(__file__)) + +# sys.path에 현재 디렉토리 추가 +if current_dir not in sys.path: + sys.path.append(current_dir) \ No newline at end of file diff --git a/backend/poetry.lock b/backend/poetry.lock index f5698c2..d7c928a 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1127,6 +1127,27 @@ files = [ [package.dependencies] prompt_toolkit = ">=2.0,<=3.0.36" +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "setuptools" version = "69.1.1" @@ -1293,6 +1314,23 @@ files = [ {file = "ujson-5.9.0.tar.gz", hash = "sha256:89cc92e73d5501b8a7f48575eeb14ad27156ad092c2e9fc7e3cf949f07e75532"}, ] +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.28.0" @@ -1594,4 +1632,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "4af0ff2503b6d87b2d58f0e73b6df658fdf4ede39b4e01e4d141f3d4e25186e2" +content-hash = "42d8579e3bc8ed70c1fdd54a36161f616cb348c863588fe7393cd69b0d71f332" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index dbf8699..a7cbd2d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -13,6 +13,7 @@ loguru = "^0.7.2" httpx = "^0.27.0" pytest = "^8.0.2" pymongo = "^4.6.2" +requests = "^2.31.0" [tool.poetry.group.dev.dependencies] diff --git a/backend/tests/api/routes/users/controller/test_user_controller.py b/backend/tests/api/routes/users/controller/test_user_controller.py index 74c386c..b71c5cd 100644 --- a/backend/tests/api/routes/users/controller/test_user_controller.py +++ b/backend/tests/api/routes/users/controller/test_user_controller.py @@ -1,7 +1,7 @@ import pytest import requests -from ......app.exception.users.signup_exeption import ( +from app.exception.users.signup_exeption import ( UserSignUpLoginIdMissningException, UserSignUpPasswordMissningException, UserSignUpNicknameMissningException, UserSignUpEmailMissningException, UserSignUpInvalidLoginIdException, UserSignUpInvalidPasswordException, diff --git a/backend/tests/database/test_database.py b/backend/tests/database/test_database.py index b7e17a6..a4eac2c 100644 --- a/backend/tests/database/test_database.py +++ b/backend/tests/database/test_database.py @@ -1,6 +1,8 @@ import pytest +import sys, os -from ...app.database.data_source import DataSource, env_file_of +# sys.path.append(os.path) +from app.database.data_source import DataSource, env_file_of from ..assert_not_raises import not_raises @pytest.mark.parametrize("input, output", [ From 06e86fed4a64f1720637909e13646ec9bd3e57fd Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 14:19:11 +0900 Subject: [PATCH 100/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=84=A0?= =?UTF-8?q?=ED=98=B8=20=ED=9B=84=EB=B3=B4=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B0=9C=EC=88=98=2016=EA=B0=9C=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #15 --- backend/app/api/routes/users/repository/user_repository.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index c147139..a12cc2d 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -48,11 +48,11 @@ def find_one(self, id: str) -> UserLoginDTO: class FoodRepository: def __init__(self): - self.collection = data_source.collection_with_name_as('foods') + self.collection = data_source.collection_with_name_as('recipes') - def find_foods(self, page_num: int, page_size: int=10) -> list: + def find_foods(self, page_num: int, page_size: int=16) -> list: skip_count: int = (page_num - 1) * page_size - results = self.collection.find().sort([('name', pymongo.ASCENDING)]).skip(skip_count).limit(page_size) + results = self.collection.find().sort([('_id', pymongo.ASCENDING)]).skip(skip_count).limit(page_size) lst = [] for result in results: result['_id'] = str(result['_id']) From 70092804758e8f9ff9ae858114f09e6e50e24ef8 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 16:35:55 +0900 Subject: [PATCH 101/187] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20?= =?UTF-8?q?=EB=82=B4=20=EC=9E=AC=EB=A3=8C=20=ED=82=A4=EA=B0=92=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20ingredient=20->=20ingredients?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- backend/app/api/routes/recipes/entity/recipe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/api/routes/recipes/entity/recipe.py b/backend/app/api/routes/recipes/entity/recipe.py index c090c1e..ecdc305 100644 --- a/backend/app/api/routes/recipes/entity/recipe.py +++ b/backend/app/api/routes/recipes/entity/recipe.py @@ -7,7 +7,7 @@ class Recipe(BaseModel): id: PyObjectId = Field(alias='_id', default=None) food_name: str recipe_name: str - ingredient: List[PyObjectId] = [] + ingredients: List[PyObjectId] = [] time_taken: int difficulty: str recipe_url: str @@ -49,7 +49,7 @@ def get_recipe_img_url(self): def get_ingredients(self): - return self.ingredient + return self.ingredients def as_basket_form(self): @@ -69,7 +69,7 @@ def as_basket_form(self): return { 'recipe_id': self.id, 'recipe_name': self.recipe_name, - 'ingredient': self.ingredient, + 'ingredient': self.ingredients, 'recipe_img_url': self.recipe_img_url, 'recipe_url': self.recipe_url, } From 3588d34cb1f525e656d2e1bd7a15cd8e7ae45f58 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 16:38:11 +0900 Subject: [PATCH 102/187] =?UTF-8?q?fix:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=83=9D=EC=84=B1=20=EA=B2=B0=EA=B3=BC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=BB=AC=EB=A0=89=EC=85=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #39 --- .../routes/users/controller/user_controller.py | 16 ++++++++++++---- .../api/routes/users/service/user_service.py | 18 +++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index b2c2d86..5f14a55 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -12,7 +12,9 @@ from .response.signup_response import SignupResponse, LoginResponse, FavorRecipesResponse from ..service.user_service import UserService from app.api.routes.recipes.service.recipes_service import RecipesService -from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository, RecommendationRepository +from ..repository.user_repository import ( + UserRepository, SessionRepository, FoodRepository, RecommendationRepository, BasketRepository +) from ..dto.user_dto import UserSignupDTO, UserLoginDTO class UserController: @@ -41,7 +43,11 @@ async def is_nickname_usable(self, nickname: str) -> bool: return self.user_service.is_nickname_usable(nickname) async def favor_recipes(self, page_num: int) -> list: - return self.user_service.favor_recipes(page_num) + foods, has_next = self.user_service.favor_recipes(page_num) + return { + 'foods': foods, + 'next_page_url': f'/api/users/foods?page_num={page_num+1}' if has_next else '' + } async def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: return self.user_service.save_favor_recipes(login_id, request) @@ -70,7 +76,7 @@ async def recommended_basket(self, user_id: str, price: int): return recommended_basket def _basket_with_infos(self, recommended_basket: dict, recipe_infos: Recipes): - logging.debug(recommended_basket) + logging.debug('recommended_basket', recommended_basket) # logging.debug(recipe_infos) recipe_info_list = [recipe.as_basket_form() for recipe in recipe_infos.get_recipes() if recipe.get_id() in recommended_basket['recipe_list']] @@ -93,7 +99,8 @@ def _basket_with_infos(self, recommended_basket: dict, recipe_infos: Recipes): UserRepository(), SessionRepository(), FoodRepository(), - RecommendationRepository()), + RecommendationRepository(), + BasketRepository()), RecipesService() ) user_router = APIRouter() @@ -140,6 +147,7 @@ async def validate_duplicate_info( @user_router.get('/api/users/foods') async def favor_recipes(page_num: int=1) -> Response: response_body = await user_controller.favor_recipes(page_num) + logging.debug(response_body) return JSONResponse(content=response_body, status_code=status.HTTP_200_OK) # POST /api/users/{user_id}/foods diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index e4b0c7e..cd53d51 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -5,7 +5,9 @@ from datetime import datetime, timedelta from ..entity.user import User -from ..repository.user_repository import UserRepository, SessionRepository, FoodRepository, RecommendationRepository +from ..repository.user_repository import ( + UserRepository, SessionRepository, FoodRepository, RecommendationRepository, BasketRepository +) from ..dto.user_dto import ( UserSignupDTO, UserLoginDTO, ) @@ -17,11 +19,13 @@ def __init__(self, session_repository: SessionRepository, food_repository: FoodRepository, recommendation_repository: RecommendationRepository, + basket_repository: BasketRepository, ): self.user_repository: UserRepository = user_repository self.session_repository: SessionRepository = session_repository self.food_repository: FoodRepository = food_repository self.recommendation_repository : RecommendationRepository = recommendation_repository + self.basket_repository: BasketRepository = basket_repository def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: return self.user_repository.insert_one(sign_up_request) @@ -64,7 +68,7 @@ def recommended_basket(self, recipe_infos: dict, price_infos: dict, price: int) return self._optimized_results(recipe_infos, price_infos, price) def save_basket(self, user_id, price, datetime, recommended_basket) -> None: - self.recommendation_repository.save({ + self.basket_repository.save({ 'user_id': user_id, 'price': price, 'datetime': datetime, @@ -122,12 +126,12 @@ def _optimized_results(self, prob.solve() # 결과 출력 - print("Status:", pulp.LpStatus[prob.status]) + # print("Status:", pulp.LpStatus[prob.status]) - for dish in dishes: - print(f"Make {dish}:", y[dish].varValue) - for ingredient in price_infos: - print(f"Use Ingredient {ingredient}:", x[ingredient].varValue) + # for dish in dishes: + # print(f"Make {dish}:", y[dish].varValue) + # for ingredient in price_infos: + # print(f"Use Ingredient {ingredient}:", x[ingredient].varValue) result_dish = [dish for dish in dishes if y[dish].varValue == 1] result_ingredient = [ingredient for ingredient in price_infos if x[ingredient].varValue == 1] From 7ff4975e2fc79fab1c699edd66a5f2617253bf3b Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 16:41:04 +0900 Subject: [PATCH 103/187] =?UTF-8?q?feat:=20=EC=9C=A0=EC=A0=80=20=EC=84=A0?= =?UTF-8?q?=ED=98=B8=20=EC=9D=8C=EC=8B=9D=20=EC=B6=94=EC=B2=9C=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=20=EC=9A=94=EA=B5=AC=20=EA=B0=9C=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20next=5Fpage=20=EB=A6=AC=ED=84=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #15 --- .../controller/request/signup_request.py | 2 +- .../users/repository/user_repository.py | 25 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/backend/app/api/routes/users/controller/request/signup_request.py b/backend/app/api/routes/users/controller/request/signup_request.py index eefd308..ee60cef 100644 --- a/backend/app/api/routes/users/controller/request/signup_request.py +++ b/backend/app/api/routes/users/controller/request/signup_request.py @@ -4,7 +4,7 @@ MINIMUM_LOGIN_ID_LENGTH = 5 MINIMUM_PASSWORD_LENGTH = 8 -MINIMUM_FAVOR_RECIPE_COUNT = 10 +MINIMUM_FAVOR_RECIPE_COUNT = 5 class SignupRequest(BaseModel): login_id: str diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index a12cc2d..a6dd2bc 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -52,17 +52,32 @@ def __init__(self): def find_foods(self, page_num: int, page_size: int=16) -> list: skip_count: int = (page_num - 1) * page_size - results = self.collection.find().sort([('_id', pymongo.ASCENDING)]).skip(skip_count).limit(page_size) + results = self.collection.find().sort([('_id', pymongo.ASCENDING)]).skip(skip_count).limit(page_size + 1) + # logging.debug(results) + results = list(results) + total_size = len(results) + logging.debug('total_size', total_size) lst = [] - for result in results: + for i, result in enumerate(results): + if i == page_size: break result['_id'] = str(result['_id']) + del result['ingredients'] lst.append(result) - return lst + + return lst, (total_size > page_size) class RecommendationRepository: def __init__(self): self.collection = data_source.collection_with_name_as('model_recommendation_histories') def find_by_login_id(self, login_id: str) -> list: - result = self.collection.find_one({'login_id': login_id}) - return result['recipe_top_20'] + result = self.collection.find_one({'id': login_id}) + return result['recommended_item'] + +class BasketRepository: + def __init__(self): + self.collection = data_source.collection_with_name_as('baskets') + + def save(self, recommendation): + self.collection.insert_one(recommendation) + \ No newline at end of file From 153707be4a5c1ebade98afa23ca1a32e1f0bd1ff Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 17:34:45 +0900 Subject: [PATCH 104/187] =?UTF-8?q?feat:=20signinpage-2=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#44?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/basket_login.py | 3 +- frontend/common.py | 2 +- frontend/newapp.py | 10 +++- frontend/signinpage_2.py | 115 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 frontend/signinpage_2.py diff --git a/frontend/basket_login.py b/frontend/basket_login.py index 17ba16e..e7998b4 100644 --- a/frontend/basket_login.py +++ b/frontend/basket_login.py @@ -45,7 +45,8 @@ def password_entered(): st.session_state.page_info = 'home2' st.session_state["token"] = { 'user_id': st.session_state['user_id'], - 'token': response['token'] + 'token': response['token'], + 'is_first_login': response['is_first_login'] } del st.session_state["password"] # Don't store the user_id or password. diff --git a/frontend/common.py b/frontend/common.py index 4d0184d..4e6fb39 100644 --- a/frontend/common.py +++ b/frontend/common.py @@ -10,7 +10,7 @@ def init(): st.session_state.is_authenticated = False st.session_state.page_info = 'home' st.session_state.url_prefix = 'http://localhost:8000' - st.session_state.url_main = 'http://175.45.194.96:8503/' + st.session_state.url_main = 'http://175.45.194.96:8502/' def set_logout_page(): st.session_state.is_authenticated = False diff --git a/frontend/newapp.py b/frontend/newapp.py index 692744a..ef795b8 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -5,6 +5,7 @@ from basket_login import login_container from main import main_page from main_2 import main_page_2 +from signinpage_2 import choose_food_page from recommendation import recommendation_page def home(): @@ -27,6 +28,10 @@ def recommendation(): page_header() recommendation_page() +def choose_food(): + page_header() + choose_food_page() + if ('is_authenticated' not in st.session_state) and ('page_info' not in st.session_state): init() @@ -37,5 +42,8 @@ def recommendation(): elif not st.session_state.get('is_authenticated', False): home() elif st.session_state.get('is_authenticated', False): - home2() + if st.session_state['token']['is_first_login']: + choose_food() + else: + home2() diff --git a/frontend/signinpage_2.py b/frontend/signinpage_2.py new file mode 100644 index 0000000..5886cdf --- /dev/null +++ b/frontend/signinpage_2.py @@ -0,0 +1,115 @@ +import math +import streamlit as st +import requests + +from main_2 import get_response + +def inquire_all_recipes(page_url): + data = get_response(page_url) + return data['foods'], data['next_page_url'] + +def show_recipes(page_num): + recipe_list, next_page_url = inquire_all_recipes(page_num) + checkboxes = display_recipes_in_four_by_four(recipe_list) + return checkboxes, next_page_url + +def add_next_page_url(next_page_url): + st.session_state['page_urls'].append(st.session_state.url_prefix + next_page_url) + +def post_cooked_recipes(cooked_recipes, all_checkboxes): + if sum(all_checkboxes.values()) < 5: + st.session_state['page_warn'] = True + return + + url = st.session_state.url_prefix + "/api/users/{user_id}/foods" + + data = { + 'recipes': cooked_recipes + } + + headers = { + 'Content-Type': 'application/json' + } + + formatted_url = url.format(user_id=st.session_state.token['user_id']) + response = requests.post(formatted_url, headers=headers, json=data) + st.session_state.token['is_first_login'] = False + if response.status_code == '201': + print('successfully updated') + else: + print(response.status_code) + +def display_recipes_in_four_by_four(recipe_list): + checkboxes = {} + + for row in range(math.ceil(len(recipe_list)/4)): + + cols = st.columns(4) + + for i in range(4): + item_idx = i + row * 4 + if item_idx >= len(recipe_list): break + + item = recipe_list[item_idx] + with cols[i]: + with st.container(border=True): + st.markdown(f'Your Image', unsafe_allow_html=True) + + + sub_cols = st.columns([1, 5]) + checkboxes[item['_id']] = st.checkbox(label=item["recipe_name"], key=f'checkbox_{item["_id"]}') + + return checkboxes + +def choose_food_page(): + + # 첫 페이지만 page_urls에 추가 + if 'page_urls' not in st.session_state: + url = st.session_state.url_prefix + "/api/users/foods?page_num={page_num}" + page_url = url.format(page_num=1) + st.session_state['page_urls'] = [page_url] + + if 'page_warn' not in st.session_state: + st.session_state['page_warn'] = False + + # 체크박스 모니터링 + all_checkboxes = dict() + + # show container + container = st.container(border=True) + + with container: + # title + st.markdown("

최근에 만들었던 음식들을 골라주세요

", unsafe_allow_html=True) + st.markdown("
5개 이상 선택해주세요
", unsafe_allow_html=True) + + for page_url in st.session_state['page_urls']: + # 16개 이미지 렌더링 + checkboxes, next_page_url = show_recipes(page_url) + # 16개에 대한 체크박스 추가 + all_checkboxes.update(checkboxes) + + # 5개 이상 체크했는지 확인 + if sum(all_checkboxes.values()) >= 5: + checked_items = [recipe for recipe, checked in all_checkboxes.items() if checked] + else: + checked_items = [] +# st.markdown("
⚠️추천 정확성을 위해 5개 이상 체크해주세요.
", unsafe_allow_html=True) + if st.session_state['page_warn']: + st.error("⚠️추천 정확성을 위해 5개 이상 체크해주세요.") + + if len(next_page_url): + cols = st.columns([3,1,1,3]) + with cols[1]: + st.button('더보기', on_click=add_next_page_url, kwargs=dict(next_page_url=next_page_url)) + with cols[2]: + st.button('다음단계', type='primary', on_click=post_cooked_recipes, args=(checked_items, all_checkboxes)) + else: + cols = st.columns([3,1,3]) + with cols[1]: + st.button('다음단계', type='primary', on_click=post_cooked_recipes, args=(checked_items, all_checkboxes)) + +#def choose_food_page(): +# +# if not choose_food_container(): +# st.stop() From 6365a763193e6597c67027643eff16347a777a78 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 17:35:27 +0900 Subject: [PATCH 105/187] =?UTF-8?q?fix:=20=ED=9E=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=B6=94=EA=B0=80=20#7=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/pages/recommendation_history.py | 1 + frontend/pages/user_history.py | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/pages/recommendation_history.py b/frontend/pages/recommendation_history.py index ff9ae5e..548da0c 100644 --- a/frontend/pages/recommendation_history.py +++ b/frontend/pages/recommendation_history.py @@ -8,4 +8,5 @@ page_header() back_to_home_container() else: + page_header() recommendation_history_page() diff --git a/frontend/pages/user_history.py b/frontend/pages/user_history.py index 6c401e8..0f716b4 100644 --- a/frontend/pages/user_history.py +++ b/frontend/pages/user_history.py @@ -8,4 +8,5 @@ page_header() back_to_home_container() else: + page_header() user_history_page() From 3a785134dfec3b358ea1358a34bfd7dadaac7075 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 18 Mar 2024 17:37:16 +0900 Subject: [PATCH 106/187] build: front-end poetry settings file update --- frontend/poetry.lock | 1908 +++++++++++++++++++++++++++++++++++++++ frontend/pyproject.toml | 20 + 2 files changed, 1928 insertions(+) create mode 100644 frontend/poetry.lock diff --git a/frontend/poetry.lock b/frontend/poetry.lock new file mode 100644 index 0000000..c7a3315 --- /dev/null +++ b/frontend/poetry.lock @@ -0,0 +1,1908 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "altair" +version = "5.2.0" +description = "Vega-Altair: A declarative statistical visualization library for Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "altair-5.2.0-py3-none-any.whl", hash = "sha256:8c4888ad11db7c39f3f17aa7f4ea985775da389d79ac30a6c22856ab238df399"}, + {file = "altair-5.2.0.tar.gz", hash = "sha256:2ad7f0c8010ebbc46319cc30febfb8e59ccf84969a201541c207bc3a4fa6cf81"}, +] + +[package.dependencies] +jinja2 = "*" +jsonschema = ">=3.0" +numpy = "*" +packaging = "*" +pandas = ">=0.25" +toolz = "*" +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["anywidget", "geopandas", "hatch", "ipython", "m2r", "mypy", "pandas-stubs", "pyarrow (>=11)", "pytest", "pytest-cov", "ruff (>=0.1.3)", "types-jsonschema", "types-setuptools", "vega-datasets", "vegafusion[embed] (>=1.4.0)", "vl-convert-python (>=1.1.0)"] +doc = ["docutils", "jinja2", "myst-parser", "numpydoc", "pillow (>=9,<10)", "pydata-sphinx-theme (>=0.14.1)", "scipy", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinxext-altair"] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "blinker" +version = "1.7.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.8" +files = [ + {file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"}, + {file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"}, +] + +[[package]] +name = "cachetools" +version = "5.3.3" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, +] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "contourpy" +version = "1.2.0" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"}, + {file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab"}, + {file = "contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488"}, + {file = "contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95"}, + {file = "contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6"}, + {file = "contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431"}, + {file = "contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f"}, + {file = "contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808"}, + {file = "contourpy-1.2.0-cp39-cp39-win32.whl", hash = "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4"}, + {file = "contourpy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956"}, + {file = "contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a"}, +] + +[package.dependencies] +numpy = ">=1.20,<2.0" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.6.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "entrypoints" +version = "0.4" +description = "Discover and load entry points from installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, + {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, +] + +[[package]] +name = "faker" +version = "24.2.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-24.2.0-py3-none-any.whl", hash = "sha256:dce4754921f9fa7e2003c26834093361b8f45072e0f46f172d6ca1234774ecd4"}, + {file = "Faker-24.2.0.tar.gz", hash = "sha256:87d5e7730426e7b36817921679c4eaf3d810cedb8c81194f47adc3df2122ca18"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + +[[package]] +name = "favicon" +version = "0.7.0" +description = "Get a website's favicon." +optional = false +python-versions = "*" +files = [ + {file = "favicon-0.7.0-py2.py3-none-any.whl", hash = "sha256:7fec0617c73dcb8521ea788e1d38cdc7226c7cb8e28c81e11625d85fa1534880"}, + {file = "favicon-0.7.0.tar.gz", hash = "sha256:6d6b5a78de2a0d0084589f687f384b2ecd6a6527093fec564403b1a30605d7a8"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.7.0" +requests = ">=2.21.0" + +[[package]] +name = "fonttools" +version = "4.50.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effd303fb422f8ce06543a36ca69148471144c534cc25f30e5be752bc4f46736"}, + {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7913992ab836f621d06aabac118fc258b9947a775a607e1a737eb3a91c360335"}, + {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0a1c5bd2f63da4043b63888534b52c5a1fd7ae187c8ffc64cbb7ae475b9dab"}, + {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40fc98540fa5360e7ecf2c56ddf3c6e7dd04929543618fd7b5cc76e66390562"}, + {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fff65fbb7afe137bac3113827855e0204482727bddd00a806034ab0d3951d0d"}, + {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1aeae3dd2ee719074a9372c89ad94f7c581903306d76befdaca2a559f802472"}, + {file = "fonttools-4.50.0-cp310-cp310-win32.whl", hash = "sha256:e9623afa319405da33b43c85cceb0585a6f5d3a1d7c604daf4f7e1dd55c03d1f"}, + {file = "fonttools-4.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:778c5f43e7e654ef7fe0605e80894930bc3a7772e2f496238e57218610140f54"}, + {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3dfb102e7f63b78c832e4539969167ffcc0375b013080e6472350965a5fe8048"}, + {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e58fe34cb379ba3d01d5d319d67dd3ce7ca9a47ad044ea2b22635cd2d1247fc"}, + {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c673ab40d15a442a4e6eb09bf007c1dda47c84ac1e2eecbdf359adacb799c24"}, + {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b3ac35cdcd1a4c90c23a5200212c1bb74fa05833cc7c14291d7043a52ca2aaa"}, + {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8844e7a2c5f7ecf977e82eb6b3014f025c8b454e046d941ece05b768be5847ae"}, + {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f849bd3c5c2249b49c98eca5aaebb920d2bfd92b3c69e84ca9bddf133e9f83f0"}, + {file = "fonttools-4.50.0-cp311-cp311-win32.whl", hash = "sha256:39293ff231b36b035575e81c14626dfc14407a20de5262f9596c2cbb199c3625"}, + {file = "fonttools-4.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:c33d5023523b44d3481624f840c8646656a1def7630ca562f222eb3ead16c438"}, + {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b4a886a6dbe60100ba1cd24de962f8cd18139bd32808da80de1fa9f9f27bf1dc"}, + {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2ca1837bfbe5eafa11313dbc7edada79052709a1fffa10cea691210af4aa1fa"}, + {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0493dd97ac8977e48ffc1476b932b37c847cbb87fd68673dee5182004906828"}, + {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77844e2f1b0889120b6c222fc49b2b75c3d88b930615e98893b899b9352a27ea"}, + {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3566bfb8c55ed9100afe1ba6f0f12265cd63a1387b9661eb6031a1578a28bad1"}, + {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:35e10ddbc129cf61775d58a14f2d44121178d89874d32cae1eac722e687d9019"}, + {file = "fonttools-4.50.0-cp312-cp312-win32.whl", hash = "sha256:cc8140baf9fa8f9b903f2b393a6c413a220fa990264b215bf48484f3d0bf8710"}, + {file = "fonttools-4.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ccc85fd96373ab73c59833b824d7a73846670a0cb1f3afbaee2b2c426a8f931"}, + {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e270a406219af37581d96c810172001ec536e29e5593aa40d4c01cca3e145aa6"}, + {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac2463de667233372e9e1c7e9de3d914b708437ef52a3199fdbf5a60184f190c"}, + {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47abd6669195abe87c22750dbcd366dc3a0648f1b7c93c2baa97429c4dc1506e"}, + {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:074841375e2e3d559aecc86e1224caf78e8b8417bb391e7d2506412538f21adc"}, + {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0743fd2191ad7ab43d78cd747215b12033ddee24fa1e088605a3efe80d6984de"}, + {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3d7080cce7be5ed65bee3496f09f79a82865a514863197ff4d4d177389e981b0"}, + {file = "fonttools-4.50.0-cp38-cp38-win32.whl", hash = "sha256:a467ba4e2eadc1d5cc1a11d355abb945f680473fbe30d15617e104c81f483045"}, + {file = "fonttools-4.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:f77e048f805e00870659d6318fd89ef28ca4ee16a22b4c5e1905b735495fc422"}, + {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6245eafd553c4e9a0708e93be51392bd2288c773523892fbd616d33fd2fda59"}, + {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a4062cc7e8de26f1603323ef3ae2171c9d29c8a9f5e067d555a2813cd5c7a7e0"}, + {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34692850dfd64ba06af61e5791a441f664cb7d21e7b544e8f385718430e8f8e4"}, + {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678dd95f26a67e02c50dcb5bf250f95231d455642afbc65a3b0bcdacd4e4dd38"}, + {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f2ce7b0b295fe64ac0a85aef46a0f2614995774bd7bc643b85679c0283287f9"}, + {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d346f4dc2221bfb7ab652d1e37d327578434ce559baf7113b0f55768437fe6a0"}, + {file = "fonttools-4.50.0-cp39-cp39-win32.whl", hash = "sha256:a51eeaf52ba3afd70bf489be20e52fdfafe6c03d652b02477c6ce23c995222f4"}, + {file = "fonttools-4.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:8639be40d583e5d9da67795aa3eeeda0488fb577a1d42ae11a5036f18fb16d93"}, + {file = "fonttools-4.50.0-py3-none-any.whl", hash = "sha256:48fa36da06247aa8282766cfd63efff1bb24e55f020f29a335939ed3844d20d3"}, + {file = "fonttools-4.50.0.tar.gz", hash = "sha256:fa5cf61058c7dbb104c2ac4e782bf1b2016a8cf2f69de6e4dd6a865d2c969bb5"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.42" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, + {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] + +[[package]] +name = "htbuilder" +version = "0.6.2" +description = "A purely-functional HTML builder for Python. Think JSX rather than templates." +optional = false +python-versions = ">=3.5" +files = [ + {file = "htbuilder-0.6.2-py3-none-any.whl", hash = "sha256:5bb707221a0e2162e406c9ecf7bcc9efa9ad590c9f2180149440415f43a10bb5"}, + {file = "htbuilder-0.6.2.tar.gz", hash = "sha256:9979a4fb6e50ce732bf6f6bc0441039dcaa3a3fc70689d8f38f601ed8a1aeec0"}, +] + +[package.dependencies] +more-itertools = "*" + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsonschema" +version = "4.21.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "lxml" +version = "5.1.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, + {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, + {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, + {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, + {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, + {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, + {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, + {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, + {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, + {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, + {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, + {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, + {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, + {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, + {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, + {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, + {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, + {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.7)"] + +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markdownlit" +version = "0.0.7" +description = "markdownlit adds a couple of lit Markdown capabilities to your Streamlit apps" +optional = false +python-versions = ">=3.6" +files = [ + {file = "markdownlit-0.0.7-py3-none-any.whl", hash = "sha256:b58bb539dcb52e0b040ab2fed32f1f3146cbb2746dc3812940d9dd359c378bb6"}, + {file = "markdownlit-0.0.7.tar.gz", hash = "sha256:553e2db454e2be4567caebef5176c98a40a7e24f7ea9c2fe8a1f05c1d9ea4005"}, +] + +[package.dependencies] +favicon = "*" +htbuilder = "*" +lxml = "*" +markdown = "*" +pymdown-extensions = "*" +streamlit = "*" +streamlit-extras = "*" + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "matplotlib" +version = "3.8.3" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f"}, + {file = "matplotlib-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f"}, + {file = "matplotlib-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635"}, + {file = "matplotlib-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65"}, + {file = "matplotlib-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0"}, + {file = "matplotlib-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1"}, + {file = "matplotlib-3.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7"}, + {file = "matplotlib-3.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01"}, + {file = "matplotlib-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb"}, + {file = "matplotlib-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26"}, + {file = "matplotlib-3.8.3.tar.gz", hash = "sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.21,<2" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pandas" +version = "2.2.1" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, + {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, + {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, + {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, + {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, + {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, + {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pillow" +version = "10.2.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "prometheus-client" +version = "0.20.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "protobuf" +version = "4.25.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, +] + +[[package]] +name = "pyarrow" +version = "15.0.1" +description = "Python library for Apache Arrow" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyarrow-15.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:c2ddb3be5ea938c329a84171694fc230b241ce1b6b0ff1a0280509af51c375fa"}, + {file = "pyarrow-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7543ea88a0ff72f8e6baaf9bfdbec2c62aeabdbede9e4a571c71cc3bc43b6302"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1519e218a6941fc074e4501088d891afcb2adf77c236e03c34babcf3d6a0d1c7"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28cafa86e1944761970d3b3fc0411b14ff9b5c2b73cd22aaf470d7a3976335f5"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be5c3d463e33d03eab496e1af7916b1d44001c08f0f458ad27dc16093a020638"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:47b1eda15d3aa3f49a07b1808648e1397e5dc6a80a30bf87faa8e2d02dad7ac3"}, + {file = "pyarrow-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e524a31be7db22deebbbcf242b189063ab9a7652c62471d296b31bc6e3cae77b"}, + {file = "pyarrow-15.0.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a476fefe8bdd56122fb0d4881b785413e025858803cc1302d0d788d3522b374d"}, + {file = "pyarrow-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:309e6191be385f2e220586bfdb643f9bb21d7e1bc6dd0a6963dc538e347b2431"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83bc586903dbeb4365cbc72b602f99f70b96c5882e5dfac5278813c7d624ca3c"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e652daac6d8b05280cd2af31c0fb61a4490ec6a53dc01588014d9fa3fdbee9"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:abad2e08652df153a72177ce20c897d083b0c4ebeec051239e2654ddf4d3c996"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cde663352bc83ad75ba7b3206e049ca1a69809223942362a8649e37bd22f9e3b"}, + {file = "pyarrow-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1b6e237dd7a08482a8b8f3f6512d258d2460f182931832a8c6ef3953203d31e1"}, + {file = "pyarrow-15.0.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd167536ee23192760b8c731d39b7cfd37914c27fd4582335ffd08450ff799d"}, + {file = "pyarrow-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c08bb31eb2984ba5c3747d375bb522e7e536b8b25b149c9cb5e1c49b0ccb736"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0f9c1d630ed2524bd1ddf28ec92780a7b599fd54704cd653519f7ff5aec177a"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5186048493395220550bca7b524420471aac2d77af831f584ce132680f55c3df"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:31dc30c7ec8958da3a3d9f31d6c3630429b2091ede0ecd0d989fd6bec129f0e4"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3f111a014fb8ac2297b43a74bf4495cc479a332908f7ee49cb7cbd50714cb0c1"}, + {file = "pyarrow-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a6d1f7c15d7f68f08490d0cb34611497c74285b8a6bbeab4ef3fc20117310983"}, + {file = "pyarrow-15.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:9ad931b996f51c2f978ed517b55cb3c6078272fb4ec579e3da5a8c14873b698d"}, + {file = "pyarrow-15.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:738f6b53ab1c2f66b2bde8a1d77e186aeaab702d849e0dfa1158c9e2c030add3"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c1c3fc16bc74e33bf8f1e5a212938ed8d88e902f372c4dac6b5bad328567d2f"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1fa92512128f6c1b8dde0468c1454dd70f3bff623970e370d52efd4d24fd0be"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b4157f307c202cbbdac147d9b07447a281fa8e63494f7fc85081da351ec6ace9"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b75e7da26f383787f80ad76143b44844ffa28648fcc7099a83df1538c078d2f2"}, + {file = "pyarrow-15.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a99eac76ae14096c209850935057b9e8ce97a78397c5cde8724674774f34e5d"}, + {file = "pyarrow-15.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:dd532d3177e031e9b2d2df19fd003d0cc0520d1747659fcabbd4d9bb87de508c"}, + {file = "pyarrow-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ce8c89848fd37e5313fc2ce601483038ee5566db96ba0808d5883b2e2e55dc53"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862eac5e5f3b6477f7a92b2f27e560e1f4e5e9edfca9ea9da8a7478bb4abd5ce"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f0ea3a29cd5cb99bf14c1c4533eceaa00ea8fb580950fb5a89a5c771a994a4e"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:bb902f780cfd624b2e8fd8501fadab17618fdb548532620ef3d91312aaf0888a"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4f87757f02735a6bb4ad2e1b98279ac45d53b748d5baf52401516413007c6999"}, + {file = "pyarrow-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:efd3816c7fbfcbd406ac0f69873cebb052effd7cdc153ae5836d1b00845845d7"}, + {file = "pyarrow-15.0.1.tar.gz", hash = "sha256:21d812548d39d490e0c6928a7c663f37b96bf764034123d4b4ab4530ecc757a9"}, +] + +[package.dependencies] +numpy = ">=1.16.6,<2" + +[[package]] +name = "pydeck" +version = "0.8.0" +description = "Widget for deck.gl maps" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydeck-0.8.0-py2.py3-none-any.whl", hash = "sha256:a8fa7757c6f24bba033af39db3147cb020eef44012ba7e60d954de187f9ed4d5"}, + {file = "pydeck-0.8.0.tar.gz", hash = "sha256:07edde833f7cfcef6749124351195aa7dcd24663d4909fd7898dbd0b6fbc01ec"}, +] + +[package.dependencies] +jinja2 = ">=2.10.1" +numpy = ">=1.16.4" + +[package.extras] +carto = ["pydeck-carto"] +jupyter = ["ipykernel (>=5.1.2)", "ipython (>=5.8.0)", "ipywidgets (>=7,<8)", "traitlets (>=4.3.2)"] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.7.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, +] + +[package.dependencies] +markdown = ">=3.5" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "referencing" +version = "0.34.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, + {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rpds-py" +version = "0.18.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, + {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, + {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, + {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, + {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, + {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, + {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, + {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, + {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, + {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, + {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, + {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, + {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "st-annotated-text" +version = "4.0.1" +description = "A simple component to display annotated text in Streamlit apps." +optional = false +python-versions = ">=3.5" +files = [ + {file = "st-annotated-text-4.0.1.tar.gz", hash = "sha256:a8ccb9a35c078ef22c6ebb244a9a0605ce27f1fd699f55939497669081b79630"}, + {file = "st_annotated_text-4.0.1-py3-none-any.whl", hash = "sha256:0a2a72903a5752a55c0acef71bdf92cd225a23a8ae4135cfc213c4538bed432f"}, +] + +[package.dependencies] +htbuilder = "*" + +[[package]] +name = "streamlit" +version = "1.32.2" +description = "A faster way to build and share data apps" +optional = false +python-versions = ">=3.8, !=3.9.7" +files = [ + {file = "streamlit-1.32.2-py2.py3-none-any.whl", hash = "sha256:a0b8044e76fec364b07be145f8b40dbd8d083e20ebbb189ceb1fa9423f3dedea"}, + {file = "streamlit-1.32.2.tar.gz", hash = "sha256:1258b9cbc3ff957bf7d09b1bfc85cedc308f1065b30748545295a9af8d5577ab"}, +] + +[package.dependencies] +altair = ">=4.0,<6" +blinker = ">=1.0.0,<2" +cachetools = ">=4.0,<6" +click = ">=7.0,<9" +gitpython = ">=3.0.7,<3.1.19 || >3.1.19,<4" +numpy = ">=1.19.3,<2" +packaging = ">=16.8,<24" +pandas = ">=1.3.0,<3" +pillow = ">=7.1.0,<11" +protobuf = ">=3.20,<5" +pyarrow = ">=7.0" +pydeck = ">=0.8.0b4,<1" +requests = ">=2.27,<3" +rich = ">=10.14.0,<14" +tenacity = ">=8.1.0,<9" +toml = ">=0.10.1,<2" +tornado = ">=6.0.3,<7" +typing-extensions = ">=4.3.0,<5" +watchdog = {version = ">=2.1.5", markers = "platform_system != \"Darwin\""} + +[package.extras] +snowflake = ["snowflake-connector-python (>=2.8.0)", "snowflake-snowpark-python (>=0.9.0)"] + +[[package]] +name = "streamlit-antd-components" +version = "0.3.2" +description = "streamlit customer components of Antd Design and Mantine" +optional = false +python-versions = ">=3.8" +files = [ + {file = "streamlit_antd_components-0.3.2-py3-none-any.whl", hash = "sha256:5ae28496127202ed266ea167649436a15f3d548a4805ee5d992c6fc0fe103fd6"}, +] + +[package.dependencies] +streamlit = ">=1.12.0" + +[[package]] +name = "streamlit-camera-input-live" +version = "0.2.0" +description = "Alternative version of st.camera_input which returns the webcam images live, without any button press needed" +optional = false +python-versions = ">=3.7" +files = [ + {file = "streamlit-camera-input-live-0.2.0.tar.gz", hash = "sha256:20ceb952b98410084176fcfeb9148e02ea29033a88d4a923161ac7890cedae0f"}, + {file = "streamlit_camera_input_live-0.2.0-py3-none-any.whl", hash = "sha256:dacb56cdedbb0d6c07e35a66b755b9145b5023e5c855c64193c3d3e73198e9be"}, +] + +[package.dependencies] +jinja2 = "*" +streamlit = ">=1.2" + +[[package]] +name = "streamlit-card" +version = "1.0.0" +description = "A streamlit component, to make UI cards" +optional = false +python-versions = ">=3.8" +files = [ + {file = "streamlit-card-1.0.0.tar.gz", hash = "sha256:be8b784d8145a4efe3c97c191db7727c96dea97912385957279ec42a7f547674"}, + {file = "streamlit_card-1.0.0-py3-none-any.whl", hash = "sha256:625ab3cd1e5368c7d9c5aeeb52a67786183e0dba940d668c556fbae01149fb3f"}, +] + +[package.dependencies] +streamlit = ">=0.63" + +[[package]] +name = "streamlit-embedcode" +version = "0.1.2" +description = "Streamlit component for embedded code snippets" +optional = false +python-versions = ">=3.6" +files = [ + {file = "streamlit-embedcode-0.1.2.tar.gz", hash = "sha256:22a50eb43407bab3d0ed2d4b58e89819da477cd0592ef87edbd373c286712e3a"}, + {file = "streamlit_embedcode-0.1.2-py3-none-any.whl", hash = "sha256:b3c9520c1b48f2eef3c702b5a967f64c9a8ff2ea8e74ebb26c0e9195965bb923"}, +] + +[package.dependencies] +streamlit = ">=0.63" + +[[package]] +name = "streamlit-extras" +version = "0.4.0" +description = "A library to discover, try, install and share Streamlit extras" +optional = false +python-versions = ">=3.8, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*" +files = [ + {file = "streamlit_extras-0.4.0-py3-none-any.whl", hash = "sha256:7371963e9472065c38cb51e79b340f1bc8995d07e0837f1ccf0930df443c2439"}, + {file = "streamlit_extras-0.4.0.tar.gz", hash = "sha256:ac67645ab84accb5ae4de8ef7ca5dd5fc69965d934b9373f813770720814204d"}, +] + +[package.dependencies] +entrypoints = ">=0.4" +htbuilder = ">=0.6.2" +markdownlit = ">=0.0.5" +prometheus-client = ">=0.14.0" +protobuf = "!=3.20.2" +st-annotated-text = ">=3.0.0" +streamlit = ">=1.0.0" +streamlit-camera-input-live = ">=0.2.0" +streamlit-card = ">=0.0.4" +streamlit-embedcode = ">=0.1.2" +streamlit-faker = ">=0.0.2" +streamlit-image-coordinates = ">=0.1.1,<0.2.0" +streamlit-keyup = ">=0.1.9" +streamlit-toggle-switch = ">=1.0.2" +streamlit-vertical-slider = ">=2.5.5" + +[[package]] +name = "streamlit-faker" +version = "0.0.3" +description = "streamlit-faker is a library to very easily fake Streamlit commands" +optional = false +python-versions = ">=3.6" +files = [ + {file = "streamlit_faker-0.0.3-py3-none-any.whl", hash = "sha256:caf410867b55b4877d8fe73cc987d089e1938f8e63594f1eb579e28015844215"}, + {file = "streamlit_faker-0.0.3.tar.gz", hash = "sha256:bff0f053aa514a99313a3699746183b41111891c82d6e9b41b1c69a7d719bf2f"}, +] + +[package.dependencies] +faker = "*" +matplotlib = "*" +streamlit = "*" +streamlit-extras = "*" + +[[package]] +name = "streamlit-image-coordinates" +version = "0.1.6" +description = "Streamlit component that displays an image and returns the coordinates when you click on it" +optional = false +python-versions = ">=3.7" +files = [ + {file = "streamlit-image-coordinates-0.1.6.tar.gz", hash = "sha256:2327599727243a5ad9798e5767b624ab4d26985f606c6be435145eb1c4127d1d"}, + {file = "streamlit_image_coordinates-0.1.6-py3-none-any.whl", hash = "sha256:56b299c38c8c9aaa2fd724f12bc3b81017d01332a242152e10eeb63dffd50dd6"}, +] + +[package.dependencies] +jinja2 = "*" +streamlit = ">=1.2" + +[[package]] +name = "streamlit-keyup" +version = "0.2.3" +description = "Text input that renders on keyup" +optional = false +python-versions = ">=3.7" +files = [ + {file = "streamlit-keyup-0.2.3.tar.gz", hash = "sha256:7b7c4fa222dd82d1a479a05683833b2959c62b2dc243ffe0a769f4b79ac5b62c"}, + {file = "streamlit_keyup-0.2.3-py3-none-any.whl", hash = "sha256:e3fe87f69b5f800d29b5fe850a064532c2af0a8b1ef34047b9d33ff6c43feaf2"}, +] + +[package.dependencies] +jinja2 = "*" +streamlit = ">=1.2" + +[[package]] +name = "streamlit-toggle-switch" +version = "1.0.2" +description = "Creates a customizable toggle" +optional = false +python-versions = ">=3.6" +files = [ + {file = "streamlit_toggle_switch-1.0.2-py3-none-any.whl", hash = "sha256:0081212d80d178bda337acf2432425e2016d757f57834b18645d4c5b928d4c0f"}, + {file = "streamlit_toggle_switch-1.0.2.tar.gz", hash = "sha256:991b103cd3448b0f6507f8051777b996a17b4630956d5b6fa13344175b20e572"}, +] + +[package.dependencies] +streamlit = ">=0.63" + +[[package]] +name = "streamlit-vertical-slider" +version = "2.5.5" +description = "Creates a customizable vertical slider" +optional = false +python-versions = ">=3.8" +files = [ + {file = "streamlit_vertical_slider-2.5.5-py3-none-any.whl", hash = "sha256:8182e861444fcd69e05c05e7109a636d459560c249f1addf78b58e525a719cb6"}, + {file = "streamlit_vertical_slider-2.5.5.tar.gz", hash = "sha256:d6854cf81a606f5c021df2037d2c49036df2d03ce5082a5227a2acca8322ca74"}, +] + +[package.dependencies] +streamlit = ">=1.22.0" + +[[package]] +name = "tenacity" +version = "8.2.3" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "toolz" +version = "0.12.1" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, + {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, +] + +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "watchdog" +version = "4.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, + {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, + {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, + {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, + {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, + {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, + {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, + {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "cb2a92d509f2bec9ddcea2640d01975c7bd1c4159399e87f58d041938b81722d" diff --git a/frontend/pyproject.toml b/frontend/pyproject.toml index 4356df4..8e750aa 100644 --- a/frontend/pyproject.toml +++ b/frontend/pyproject.toml @@ -5,3 +5,23 @@ version_scheme = "pep440" version = "0.1.0" update_changelog_on_bump = true major_version_zero = true + +[tool.poetry] +name = "frontend" +version = "0.1.0" +description = "basket recommendation front end" +authors = ["Judy "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" + + +[tool.poetry.group.dev.dependencies] +streamlit = "^1.32.2" +streamlit-antd-components = "^0.3.2" +streamlit-extras = "^0.4.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From ec6c321a757d513fc5f04d9f2afc91dcdf9dfc95 Mon Sep 17 00:00:00 2001 From: Judy Date: Mon, 18 Mar 2024 17:34:45 +0900 Subject: [PATCH 107/187] =?UTF-8?q?feat:=20signinpage-2=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#44?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/basket_login.py | 3 +- frontend/common.py | 2 +- frontend/newapp.py | 10 +++- frontend/signinpage_2.py | 115 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 frontend/signinpage_2.py diff --git a/frontend/basket_login.py b/frontend/basket_login.py index 17ba16e..e7998b4 100644 --- a/frontend/basket_login.py +++ b/frontend/basket_login.py @@ -45,7 +45,8 @@ def password_entered(): st.session_state.page_info = 'home2' st.session_state["token"] = { 'user_id': st.session_state['user_id'], - 'token': response['token'] + 'token': response['token'], + 'is_first_login': response['is_first_login'] } del st.session_state["password"] # Don't store the user_id or password. diff --git a/frontend/common.py b/frontend/common.py index 4d0184d..4e6fb39 100644 --- a/frontend/common.py +++ b/frontend/common.py @@ -10,7 +10,7 @@ def init(): st.session_state.is_authenticated = False st.session_state.page_info = 'home' st.session_state.url_prefix = 'http://localhost:8000' - st.session_state.url_main = 'http://175.45.194.96:8503/' + st.session_state.url_main = 'http://175.45.194.96:8502/' def set_logout_page(): st.session_state.is_authenticated = False diff --git a/frontend/newapp.py b/frontend/newapp.py index 692744a..ef795b8 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -5,6 +5,7 @@ from basket_login import login_container from main import main_page from main_2 import main_page_2 +from signinpage_2 import choose_food_page from recommendation import recommendation_page def home(): @@ -27,6 +28,10 @@ def recommendation(): page_header() recommendation_page() +def choose_food(): + page_header() + choose_food_page() + if ('is_authenticated' not in st.session_state) and ('page_info' not in st.session_state): init() @@ -37,5 +42,8 @@ def recommendation(): elif not st.session_state.get('is_authenticated', False): home() elif st.session_state.get('is_authenticated', False): - home2() + if st.session_state['token']['is_first_login']: + choose_food() + else: + home2() diff --git a/frontend/signinpage_2.py b/frontend/signinpage_2.py new file mode 100644 index 0000000..5886cdf --- /dev/null +++ b/frontend/signinpage_2.py @@ -0,0 +1,115 @@ +import math +import streamlit as st +import requests + +from main_2 import get_response + +def inquire_all_recipes(page_url): + data = get_response(page_url) + return data['foods'], data['next_page_url'] + +def show_recipes(page_num): + recipe_list, next_page_url = inquire_all_recipes(page_num) + checkboxes = display_recipes_in_four_by_four(recipe_list) + return checkboxes, next_page_url + +def add_next_page_url(next_page_url): + st.session_state['page_urls'].append(st.session_state.url_prefix + next_page_url) + +def post_cooked_recipes(cooked_recipes, all_checkboxes): + if sum(all_checkboxes.values()) < 5: + st.session_state['page_warn'] = True + return + + url = st.session_state.url_prefix + "/api/users/{user_id}/foods" + + data = { + 'recipes': cooked_recipes + } + + headers = { + 'Content-Type': 'application/json' + } + + formatted_url = url.format(user_id=st.session_state.token['user_id']) + response = requests.post(formatted_url, headers=headers, json=data) + st.session_state.token['is_first_login'] = False + if response.status_code == '201': + print('successfully updated') + else: + print(response.status_code) + +def display_recipes_in_four_by_four(recipe_list): + checkboxes = {} + + for row in range(math.ceil(len(recipe_list)/4)): + + cols = st.columns(4) + + for i in range(4): + item_idx = i + row * 4 + if item_idx >= len(recipe_list): break + + item = recipe_list[item_idx] + with cols[i]: + with st.container(border=True): + st.markdown(f'Your Image', unsafe_allow_html=True) + + + sub_cols = st.columns([1, 5]) + checkboxes[item['_id']] = st.checkbox(label=item["recipe_name"], key=f'checkbox_{item["_id"]}') + + return checkboxes + +def choose_food_page(): + + # 첫 페이지만 page_urls에 추가 + if 'page_urls' not in st.session_state: + url = st.session_state.url_prefix + "/api/users/foods?page_num={page_num}" + page_url = url.format(page_num=1) + st.session_state['page_urls'] = [page_url] + + if 'page_warn' not in st.session_state: + st.session_state['page_warn'] = False + + # 체크박스 모니터링 + all_checkboxes = dict() + + # show container + container = st.container(border=True) + + with container: + # title + st.markdown("

최근에 만들었던 음식들을 골라주세요

", unsafe_allow_html=True) + st.markdown("
5개 이상 선택해주세요
", unsafe_allow_html=True) + + for page_url in st.session_state['page_urls']: + # 16개 이미지 렌더링 + checkboxes, next_page_url = show_recipes(page_url) + # 16개에 대한 체크박스 추가 + all_checkboxes.update(checkboxes) + + # 5개 이상 체크했는지 확인 + if sum(all_checkboxes.values()) >= 5: + checked_items = [recipe for recipe, checked in all_checkboxes.items() if checked] + else: + checked_items = [] +# st.markdown("
⚠️추천 정확성을 위해 5개 이상 체크해주세요.
", unsafe_allow_html=True) + if st.session_state['page_warn']: + st.error("⚠️추천 정확성을 위해 5개 이상 체크해주세요.") + + if len(next_page_url): + cols = st.columns([3,1,1,3]) + with cols[1]: + st.button('더보기', on_click=add_next_page_url, kwargs=dict(next_page_url=next_page_url)) + with cols[2]: + st.button('다음단계', type='primary', on_click=post_cooked_recipes, args=(checked_items, all_checkboxes)) + else: + cols = st.columns([3,1,3]) + with cols[1]: + st.button('다음단계', type='primary', on_click=post_cooked_recipes, args=(checked_items, all_checkboxes)) + +#def choose_food_page(): +# +# if not choose_food_container(): +# st.stop() From a31f1c53400a79dc53a001645bbeb97fd7eb2caf Mon Sep 17 00:00:00 2001 From: Judy Date: Mon, 18 Mar 2024 17:35:27 +0900 Subject: [PATCH 108/187] =?UTF-8?q?fix:=20=ED=9E=88=EC=8A=A4=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=B6=94=EA=B0=80=20#7=20#8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/pages/recommendation_history.py | 1 + frontend/pages/user_history.py | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/pages/recommendation_history.py b/frontend/pages/recommendation_history.py index ff9ae5e..548da0c 100644 --- a/frontend/pages/recommendation_history.py +++ b/frontend/pages/recommendation_history.py @@ -8,4 +8,5 @@ page_header() back_to_home_container() else: + page_header() recommendation_history_page() diff --git a/frontend/pages/user_history.py b/frontend/pages/user_history.py index 6c401e8..0f716b4 100644 --- a/frontend/pages/user_history.py +++ b/frontend/pages/user_history.py @@ -8,4 +8,5 @@ page_header() back_to_home_container() else: + page_header() user_history_page() From 8110ef77acb79690b2329caf59a4595d137136df Mon Sep 17 00:00:00 2001 From: Judy Date: Mon, 18 Mar 2024 17:37:16 +0900 Subject: [PATCH 109/187] build: front-end poetry settings file update --- frontend/poetry.lock | 1908 +++++++++++++++++++++++++++++++++++++++ frontend/pyproject.toml | 20 + 2 files changed, 1928 insertions(+) create mode 100644 frontend/poetry.lock diff --git a/frontend/poetry.lock b/frontend/poetry.lock new file mode 100644 index 0000000..c7a3315 --- /dev/null +++ b/frontend/poetry.lock @@ -0,0 +1,1908 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "altair" +version = "5.2.0" +description = "Vega-Altair: A declarative statistical visualization library for Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "altair-5.2.0-py3-none-any.whl", hash = "sha256:8c4888ad11db7c39f3f17aa7f4ea985775da389d79ac30a6c22856ab238df399"}, + {file = "altair-5.2.0.tar.gz", hash = "sha256:2ad7f0c8010ebbc46319cc30febfb8e59ccf84969a201541c207bc3a4fa6cf81"}, +] + +[package.dependencies] +jinja2 = "*" +jsonschema = ">=3.0" +numpy = "*" +packaging = "*" +pandas = ">=0.25" +toolz = "*" +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["anywidget", "geopandas", "hatch", "ipython", "m2r", "mypy", "pandas-stubs", "pyarrow (>=11)", "pytest", "pytest-cov", "ruff (>=0.1.3)", "types-jsonschema", "types-setuptools", "vega-datasets", "vegafusion[embed] (>=1.4.0)", "vl-convert-python (>=1.1.0)"] +doc = ["docutils", "jinja2", "myst-parser", "numpydoc", "pillow (>=9,<10)", "pydata-sphinx-theme (>=0.14.1)", "scipy", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinxext-altair"] + +[[package]] +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "blinker" +version = "1.7.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.8" +files = [ + {file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"}, + {file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"}, +] + +[[package]] +name = "cachetools" +version = "5.3.3" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, + {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, +] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "contourpy" +version = "1.2.0" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"}, + {file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9"}, + {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9"}, + {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab"}, + {file = "contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488"}, + {file = "contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727"}, + {file = "contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e"}, + {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286"}, + {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95"}, + {file = "contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6"}, + {file = "contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0"}, + {file = "contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399"}, + {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0"}, + {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431"}, + {file = "contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f"}, + {file = "contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc"}, + {file = "contourpy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8"}, + {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e"}, + {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808"}, + {file = "contourpy-1.2.0-cp39-cp39-win32.whl", hash = "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4"}, + {file = "contourpy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776"}, + {file = "contourpy-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956"}, + {file = "contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a"}, +] + +[package.dependencies] +numpy = ">=1.20,<2.0" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.6.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "entrypoints" +version = "0.4" +description = "Discover and load entry points from installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, + {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, +] + +[[package]] +name = "faker" +version = "24.2.0" +description = "Faker is a Python package that generates fake data for you." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Faker-24.2.0-py3-none-any.whl", hash = "sha256:dce4754921f9fa7e2003c26834093361b8f45072e0f46f172d6ca1234774ecd4"}, + {file = "Faker-24.2.0.tar.gz", hash = "sha256:87d5e7730426e7b36817921679c4eaf3d810cedb8c81194f47adc3df2122ca18"}, +] + +[package.dependencies] +python-dateutil = ">=2.4" + +[[package]] +name = "favicon" +version = "0.7.0" +description = "Get a website's favicon." +optional = false +python-versions = "*" +files = [ + {file = "favicon-0.7.0-py2.py3-none-any.whl", hash = "sha256:7fec0617c73dcb8521ea788e1d38cdc7226c7cb8e28c81e11625d85fa1534880"}, + {file = "favicon-0.7.0.tar.gz", hash = "sha256:6d6b5a78de2a0d0084589f687f384b2ecd6a6527093fec564403b1a30605d7a8"}, +] + +[package.dependencies] +beautifulsoup4 = ">=4.7.0" +requests = ">=2.21.0" + +[[package]] +name = "fonttools" +version = "4.50.0" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effd303fb422f8ce06543a36ca69148471144c534cc25f30e5be752bc4f46736"}, + {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7913992ab836f621d06aabac118fc258b9947a775a607e1a737eb3a91c360335"}, + {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0a1c5bd2f63da4043b63888534b52c5a1fd7ae187c8ffc64cbb7ae475b9dab"}, + {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40fc98540fa5360e7ecf2c56ddf3c6e7dd04929543618fd7b5cc76e66390562"}, + {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fff65fbb7afe137bac3113827855e0204482727bddd00a806034ab0d3951d0d"}, + {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1aeae3dd2ee719074a9372c89ad94f7c581903306d76befdaca2a559f802472"}, + {file = "fonttools-4.50.0-cp310-cp310-win32.whl", hash = "sha256:e9623afa319405da33b43c85cceb0585a6f5d3a1d7c604daf4f7e1dd55c03d1f"}, + {file = "fonttools-4.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:778c5f43e7e654ef7fe0605e80894930bc3a7772e2f496238e57218610140f54"}, + {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3dfb102e7f63b78c832e4539969167ffcc0375b013080e6472350965a5fe8048"}, + {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e58fe34cb379ba3d01d5d319d67dd3ce7ca9a47ad044ea2b22635cd2d1247fc"}, + {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c673ab40d15a442a4e6eb09bf007c1dda47c84ac1e2eecbdf359adacb799c24"}, + {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b3ac35cdcd1a4c90c23a5200212c1bb74fa05833cc7c14291d7043a52ca2aaa"}, + {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8844e7a2c5f7ecf977e82eb6b3014f025c8b454e046d941ece05b768be5847ae"}, + {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f849bd3c5c2249b49c98eca5aaebb920d2bfd92b3c69e84ca9bddf133e9f83f0"}, + {file = "fonttools-4.50.0-cp311-cp311-win32.whl", hash = "sha256:39293ff231b36b035575e81c14626dfc14407a20de5262f9596c2cbb199c3625"}, + {file = "fonttools-4.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:c33d5023523b44d3481624f840c8646656a1def7630ca562f222eb3ead16c438"}, + {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b4a886a6dbe60100ba1cd24de962f8cd18139bd32808da80de1fa9f9f27bf1dc"}, + {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2ca1837bfbe5eafa11313dbc7edada79052709a1fffa10cea691210af4aa1fa"}, + {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0493dd97ac8977e48ffc1476b932b37c847cbb87fd68673dee5182004906828"}, + {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77844e2f1b0889120b6c222fc49b2b75c3d88b930615e98893b899b9352a27ea"}, + {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3566bfb8c55ed9100afe1ba6f0f12265cd63a1387b9661eb6031a1578a28bad1"}, + {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:35e10ddbc129cf61775d58a14f2d44121178d89874d32cae1eac722e687d9019"}, + {file = "fonttools-4.50.0-cp312-cp312-win32.whl", hash = "sha256:cc8140baf9fa8f9b903f2b393a6c413a220fa990264b215bf48484f3d0bf8710"}, + {file = "fonttools-4.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ccc85fd96373ab73c59833b824d7a73846670a0cb1f3afbaee2b2c426a8f931"}, + {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e270a406219af37581d96c810172001ec536e29e5593aa40d4c01cca3e145aa6"}, + {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac2463de667233372e9e1c7e9de3d914b708437ef52a3199fdbf5a60184f190c"}, + {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47abd6669195abe87c22750dbcd366dc3a0648f1b7c93c2baa97429c4dc1506e"}, + {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:074841375e2e3d559aecc86e1224caf78e8b8417bb391e7d2506412538f21adc"}, + {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0743fd2191ad7ab43d78cd747215b12033ddee24fa1e088605a3efe80d6984de"}, + {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3d7080cce7be5ed65bee3496f09f79a82865a514863197ff4d4d177389e981b0"}, + {file = "fonttools-4.50.0-cp38-cp38-win32.whl", hash = "sha256:a467ba4e2eadc1d5cc1a11d355abb945f680473fbe30d15617e104c81f483045"}, + {file = "fonttools-4.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:f77e048f805e00870659d6318fd89ef28ca4ee16a22b4c5e1905b735495fc422"}, + {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6245eafd553c4e9a0708e93be51392bd2288c773523892fbd616d33fd2fda59"}, + {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a4062cc7e8de26f1603323ef3ae2171c9d29c8a9f5e067d555a2813cd5c7a7e0"}, + {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34692850dfd64ba06af61e5791a441f664cb7d21e7b544e8f385718430e8f8e4"}, + {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678dd95f26a67e02c50dcb5bf250f95231d455642afbc65a3b0bcdacd4e4dd38"}, + {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f2ce7b0b295fe64ac0a85aef46a0f2614995774bd7bc643b85679c0283287f9"}, + {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d346f4dc2221bfb7ab652d1e37d327578434ce559baf7113b0f55768437fe6a0"}, + {file = "fonttools-4.50.0-cp39-cp39-win32.whl", hash = "sha256:a51eeaf52ba3afd70bf489be20e52fdfafe6c03d652b02477c6ce23c995222f4"}, + {file = "fonttools-4.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:8639be40d583e5d9da67795aa3eeeda0488fb577a1d42ae11a5036f18fb16d93"}, + {file = "fonttools-4.50.0-py3-none-any.whl", hash = "sha256:48fa36da06247aa8282766cfd63efff1bb24e55f020f29a335939ed3844d20d3"}, + {file = "fonttools-4.50.0.tar.gz", hash = "sha256:fa5cf61058c7dbb104c2ac4e782bf1b2016a8cf2f69de6e4dd6a865d2c969bb5"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "gitdb" +version = "4.0.11" +description = "Git Object Database" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gitdb-4.0.11-py3-none-any.whl", hash = "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4"}, + {file = "gitdb-4.0.11.tar.gz", hash = "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b"}, +] + +[package.dependencies] +smmap = ">=3.0.1,<6" + +[[package]] +name = "gitpython" +version = "3.1.42" +description = "GitPython is a Python library used to interact with Git repositories" +optional = false +python-versions = ">=3.7" +files = [ + {file = "GitPython-3.1.42-py3-none-any.whl", hash = "sha256:1bf9cd7c9e7255f77778ea54359e54ac22a72a5b51288c457c881057b7bb9ecd"}, + {file = "GitPython-3.1.42.tar.gz", hash = "sha256:2d99869e0fef71a73cbd242528105af1d6c1b108c60dfabd994bf292f76c3ceb"}, +] + +[package.dependencies] +gitdb = ">=4.0.1,<5" + +[package.extras] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar"] + +[[package]] +name = "htbuilder" +version = "0.6.2" +description = "A purely-functional HTML builder for Python. Think JSX rather than templates." +optional = false +python-versions = ">=3.5" +files = [ + {file = "htbuilder-0.6.2-py3-none-any.whl", hash = "sha256:5bb707221a0e2162e406c9ecf7bcc9efa9ad590c9f2180149440415f43a10bb5"}, + {file = "htbuilder-0.6.2.tar.gz", hash = "sha256:9979a4fb6e50ce732bf6f6bc0441039dcaa3a3fc70689d8f38f601ed8a1aeec0"}, +] + +[package.dependencies] +more-itertools = "*" + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jsonschema" +version = "4.21.1" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] + +[[package]] +name = "jsonschema-specifications" +version = "2023.12.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "kiwisolver" +version = "1.4.5" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.7" +files = [ + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +] + +[[package]] +name = "lxml" +version = "5.1.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.6" +files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, + {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, + {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, + {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, + {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, + {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, + {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, + {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, + {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, + {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, + {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, + {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, + {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, + {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, + {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, + {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, + {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, + {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.7)"] + +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markdownlit" +version = "0.0.7" +description = "markdownlit adds a couple of lit Markdown capabilities to your Streamlit apps" +optional = false +python-versions = ">=3.6" +files = [ + {file = "markdownlit-0.0.7-py3-none-any.whl", hash = "sha256:b58bb539dcb52e0b040ab2fed32f1f3146cbb2746dc3812940d9dd359c378bb6"}, + {file = "markdownlit-0.0.7.tar.gz", hash = "sha256:553e2db454e2be4567caebef5176c98a40a7e24f7ea9c2fe8a1f05c1d9ea4005"}, +] + +[package.dependencies] +favicon = "*" +htbuilder = "*" +lxml = "*" +markdown = "*" +pymdown-extensions = "*" +streamlit = "*" +streamlit-extras = "*" + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "matplotlib" +version = "3.8.3" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f"}, + {file = "matplotlib-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec"}, + {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f"}, + {file = "matplotlib-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635"}, + {file = "matplotlib-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900"}, + {file = "matplotlib-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7"}, + {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65"}, + {file = "matplotlib-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0"}, + {file = "matplotlib-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4"}, + {file = "matplotlib-3.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5"}, + {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1"}, + {file = "matplotlib-3.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7"}, + {file = "matplotlib-3.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4"}, + {file = "matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7"}, + {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01"}, + {file = "matplotlib-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb"}, + {file = "matplotlib-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc"}, + {file = "matplotlib-3.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26"}, + {file = "matplotlib-3.8.3.tar.gz", hash = "sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.21,<2" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.2.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.8" +files = [ + {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, + {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pandas" +version = "2.2.1" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, + {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, + {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, + {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, + {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, + {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, + {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pillow" +version = "10.2.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "prometheus-client" +version = "0.20.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "protobuf" +version = "4.25.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, +] + +[[package]] +name = "pyarrow" +version = "15.0.1" +description = "Python library for Apache Arrow" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyarrow-15.0.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:c2ddb3be5ea938c329a84171694fc230b241ce1b6b0ff1a0280509af51c375fa"}, + {file = "pyarrow-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7543ea88a0ff72f8e6baaf9bfdbec2c62aeabdbede9e4a571c71cc3bc43b6302"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1519e218a6941fc074e4501088d891afcb2adf77c236e03c34babcf3d6a0d1c7"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28cafa86e1944761970d3b3fc0411b14ff9b5c2b73cd22aaf470d7a3976335f5"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:be5c3d463e33d03eab496e1af7916b1d44001c08f0f458ad27dc16093a020638"}, + {file = "pyarrow-15.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:47b1eda15d3aa3f49a07b1808648e1397e5dc6a80a30bf87faa8e2d02dad7ac3"}, + {file = "pyarrow-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e524a31be7db22deebbbcf242b189063ab9a7652c62471d296b31bc6e3cae77b"}, + {file = "pyarrow-15.0.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a476fefe8bdd56122fb0d4881b785413e025858803cc1302d0d788d3522b374d"}, + {file = "pyarrow-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:309e6191be385f2e220586bfdb643f9bb21d7e1bc6dd0a6963dc538e347b2431"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83bc586903dbeb4365cbc72b602f99f70b96c5882e5dfac5278813c7d624ca3c"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e652daac6d8b05280cd2af31c0fb61a4490ec6a53dc01588014d9fa3fdbee9"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:abad2e08652df153a72177ce20c897d083b0c4ebeec051239e2654ddf4d3c996"}, + {file = "pyarrow-15.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cde663352bc83ad75ba7b3206e049ca1a69809223942362a8649e37bd22f9e3b"}, + {file = "pyarrow-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:1b6e237dd7a08482a8b8f3f6512d258d2460f182931832a8c6ef3953203d31e1"}, + {file = "pyarrow-15.0.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd167536ee23192760b8c731d39b7cfd37914c27fd4582335ffd08450ff799d"}, + {file = "pyarrow-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c08bb31eb2984ba5c3747d375bb522e7e536b8b25b149c9cb5e1c49b0ccb736"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0f9c1d630ed2524bd1ddf28ec92780a7b599fd54704cd653519f7ff5aec177a"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5186048493395220550bca7b524420471aac2d77af831f584ce132680f55c3df"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:31dc30c7ec8958da3a3d9f31d6c3630429b2091ede0ecd0d989fd6bec129f0e4"}, + {file = "pyarrow-15.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3f111a014fb8ac2297b43a74bf4495cc479a332908f7ee49cb7cbd50714cb0c1"}, + {file = "pyarrow-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a6d1f7c15d7f68f08490d0cb34611497c74285b8a6bbeab4ef3fc20117310983"}, + {file = "pyarrow-15.0.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:9ad931b996f51c2f978ed517b55cb3c6078272fb4ec579e3da5a8c14873b698d"}, + {file = "pyarrow-15.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:738f6b53ab1c2f66b2bde8a1d77e186aeaab702d849e0dfa1158c9e2c030add3"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c1c3fc16bc74e33bf8f1e5a212938ed8d88e902f372c4dac6b5bad328567d2f"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1fa92512128f6c1b8dde0468c1454dd70f3bff623970e370d52efd4d24fd0be"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:b4157f307c202cbbdac147d9b07447a281fa8e63494f7fc85081da351ec6ace9"}, + {file = "pyarrow-15.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:b75e7da26f383787f80ad76143b44844ffa28648fcc7099a83df1538c078d2f2"}, + {file = "pyarrow-15.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:3a99eac76ae14096c209850935057b9e8ce97a78397c5cde8724674774f34e5d"}, + {file = "pyarrow-15.0.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:dd532d3177e031e9b2d2df19fd003d0cc0520d1747659fcabbd4d9bb87de508c"}, + {file = "pyarrow-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ce8c89848fd37e5313fc2ce601483038ee5566db96ba0808d5883b2e2e55dc53"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:862eac5e5f3b6477f7a92b2f27e560e1f4e5e9edfca9ea9da8a7478bb4abd5ce"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f0ea3a29cd5cb99bf14c1c4533eceaa00ea8fb580950fb5a89a5c771a994a4e"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:bb902f780cfd624b2e8fd8501fadab17618fdb548532620ef3d91312aaf0888a"}, + {file = "pyarrow-15.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:4f87757f02735a6bb4ad2e1b98279ac45d53b748d5baf52401516413007c6999"}, + {file = "pyarrow-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:efd3816c7fbfcbd406ac0f69873cebb052effd7cdc153ae5836d1b00845845d7"}, + {file = "pyarrow-15.0.1.tar.gz", hash = "sha256:21d812548d39d490e0c6928a7c663f37b96bf764034123d4b4ab4530ecc757a9"}, +] + +[package.dependencies] +numpy = ">=1.16.6,<2" + +[[package]] +name = "pydeck" +version = "0.8.0" +description = "Widget for deck.gl maps" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydeck-0.8.0-py2.py3-none-any.whl", hash = "sha256:a8fa7757c6f24bba033af39db3147cb020eef44012ba7e60d954de187f9ed4d5"}, + {file = "pydeck-0.8.0.tar.gz", hash = "sha256:07edde833f7cfcef6749124351195aa7dcd24663d4909fd7898dbd0b6fbc01ec"}, +] + +[package.dependencies] +jinja2 = ">=2.10.1" +numpy = ">=1.16.4" + +[package.extras] +carto = ["pydeck-carto"] +jupyter = ["ipykernel (>=5.1.2)", "ipython (>=5.8.0)", "ipywidgets (>=7,<8)", "traitlets (>=4.3.2)"] + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.7.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, +] + +[package.dependencies] +markdown = ">=3.5" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "referencing" +version = "0.34.0" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.34.0-py3-none-any.whl", hash = "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4"}, + {file = "referencing-0.34.0.tar.gz", hash = "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "rpds-py" +version = "0.18.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, + {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, + {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, + {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, + {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, + {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, + {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, + {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, + {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, + {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, + {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, + {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, + {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, + {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, + {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, + {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, + {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, + {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, + {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, + {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, + {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, + {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, + {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, + {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, + {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, + {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, + {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, + {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, + {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, + {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smmap" +version = "5.0.1" +description = "A pure Python implementation of a sliding window memory map manager" +optional = false +python-versions = ">=3.7" +files = [ + {file = "smmap-5.0.1-py3-none-any.whl", hash = "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da"}, + {file = "smmap-5.0.1.tar.gz", hash = "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "st-annotated-text" +version = "4.0.1" +description = "A simple component to display annotated text in Streamlit apps." +optional = false +python-versions = ">=3.5" +files = [ + {file = "st-annotated-text-4.0.1.tar.gz", hash = "sha256:a8ccb9a35c078ef22c6ebb244a9a0605ce27f1fd699f55939497669081b79630"}, + {file = "st_annotated_text-4.0.1-py3-none-any.whl", hash = "sha256:0a2a72903a5752a55c0acef71bdf92cd225a23a8ae4135cfc213c4538bed432f"}, +] + +[package.dependencies] +htbuilder = "*" + +[[package]] +name = "streamlit" +version = "1.32.2" +description = "A faster way to build and share data apps" +optional = false +python-versions = ">=3.8, !=3.9.7" +files = [ + {file = "streamlit-1.32.2-py2.py3-none-any.whl", hash = "sha256:a0b8044e76fec364b07be145f8b40dbd8d083e20ebbb189ceb1fa9423f3dedea"}, + {file = "streamlit-1.32.2.tar.gz", hash = "sha256:1258b9cbc3ff957bf7d09b1bfc85cedc308f1065b30748545295a9af8d5577ab"}, +] + +[package.dependencies] +altair = ">=4.0,<6" +blinker = ">=1.0.0,<2" +cachetools = ">=4.0,<6" +click = ">=7.0,<9" +gitpython = ">=3.0.7,<3.1.19 || >3.1.19,<4" +numpy = ">=1.19.3,<2" +packaging = ">=16.8,<24" +pandas = ">=1.3.0,<3" +pillow = ">=7.1.0,<11" +protobuf = ">=3.20,<5" +pyarrow = ">=7.0" +pydeck = ">=0.8.0b4,<1" +requests = ">=2.27,<3" +rich = ">=10.14.0,<14" +tenacity = ">=8.1.0,<9" +toml = ">=0.10.1,<2" +tornado = ">=6.0.3,<7" +typing-extensions = ">=4.3.0,<5" +watchdog = {version = ">=2.1.5", markers = "platform_system != \"Darwin\""} + +[package.extras] +snowflake = ["snowflake-connector-python (>=2.8.0)", "snowflake-snowpark-python (>=0.9.0)"] + +[[package]] +name = "streamlit-antd-components" +version = "0.3.2" +description = "streamlit customer components of Antd Design and Mantine" +optional = false +python-versions = ">=3.8" +files = [ + {file = "streamlit_antd_components-0.3.2-py3-none-any.whl", hash = "sha256:5ae28496127202ed266ea167649436a15f3d548a4805ee5d992c6fc0fe103fd6"}, +] + +[package.dependencies] +streamlit = ">=1.12.0" + +[[package]] +name = "streamlit-camera-input-live" +version = "0.2.0" +description = "Alternative version of st.camera_input which returns the webcam images live, without any button press needed" +optional = false +python-versions = ">=3.7" +files = [ + {file = "streamlit-camera-input-live-0.2.0.tar.gz", hash = "sha256:20ceb952b98410084176fcfeb9148e02ea29033a88d4a923161ac7890cedae0f"}, + {file = "streamlit_camera_input_live-0.2.0-py3-none-any.whl", hash = "sha256:dacb56cdedbb0d6c07e35a66b755b9145b5023e5c855c64193c3d3e73198e9be"}, +] + +[package.dependencies] +jinja2 = "*" +streamlit = ">=1.2" + +[[package]] +name = "streamlit-card" +version = "1.0.0" +description = "A streamlit component, to make UI cards" +optional = false +python-versions = ">=3.8" +files = [ + {file = "streamlit-card-1.0.0.tar.gz", hash = "sha256:be8b784d8145a4efe3c97c191db7727c96dea97912385957279ec42a7f547674"}, + {file = "streamlit_card-1.0.0-py3-none-any.whl", hash = "sha256:625ab3cd1e5368c7d9c5aeeb52a67786183e0dba940d668c556fbae01149fb3f"}, +] + +[package.dependencies] +streamlit = ">=0.63" + +[[package]] +name = "streamlit-embedcode" +version = "0.1.2" +description = "Streamlit component for embedded code snippets" +optional = false +python-versions = ">=3.6" +files = [ + {file = "streamlit-embedcode-0.1.2.tar.gz", hash = "sha256:22a50eb43407bab3d0ed2d4b58e89819da477cd0592ef87edbd373c286712e3a"}, + {file = "streamlit_embedcode-0.1.2-py3-none-any.whl", hash = "sha256:b3c9520c1b48f2eef3c702b5a967f64c9a8ff2ea8e74ebb26c0e9195965bb923"}, +] + +[package.dependencies] +streamlit = ">=0.63" + +[[package]] +name = "streamlit-extras" +version = "0.4.0" +description = "A library to discover, try, install and share Streamlit extras" +optional = false +python-versions = ">=3.8, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*" +files = [ + {file = "streamlit_extras-0.4.0-py3-none-any.whl", hash = "sha256:7371963e9472065c38cb51e79b340f1bc8995d07e0837f1ccf0930df443c2439"}, + {file = "streamlit_extras-0.4.0.tar.gz", hash = "sha256:ac67645ab84accb5ae4de8ef7ca5dd5fc69965d934b9373f813770720814204d"}, +] + +[package.dependencies] +entrypoints = ">=0.4" +htbuilder = ">=0.6.2" +markdownlit = ">=0.0.5" +prometheus-client = ">=0.14.0" +protobuf = "!=3.20.2" +st-annotated-text = ">=3.0.0" +streamlit = ">=1.0.0" +streamlit-camera-input-live = ">=0.2.0" +streamlit-card = ">=0.0.4" +streamlit-embedcode = ">=0.1.2" +streamlit-faker = ">=0.0.2" +streamlit-image-coordinates = ">=0.1.1,<0.2.0" +streamlit-keyup = ">=0.1.9" +streamlit-toggle-switch = ">=1.0.2" +streamlit-vertical-slider = ">=2.5.5" + +[[package]] +name = "streamlit-faker" +version = "0.0.3" +description = "streamlit-faker is a library to very easily fake Streamlit commands" +optional = false +python-versions = ">=3.6" +files = [ + {file = "streamlit_faker-0.0.3-py3-none-any.whl", hash = "sha256:caf410867b55b4877d8fe73cc987d089e1938f8e63594f1eb579e28015844215"}, + {file = "streamlit_faker-0.0.3.tar.gz", hash = "sha256:bff0f053aa514a99313a3699746183b41111891c82d6e9b41b1c69a7d719bf2f"}, +] + +[package.dependencies] +faker = "*" +matplotlib = "*" +streamlit = "*" +streamlit-extras = "*" + +[[package]] +name = "streamlit-image-coordinates" +version = "0.1.6" +description = "Streamlit component that displays an image and returns the coordinates when you click on it" +optional = false +python-versions = ">=3.7" +files = [ + {file = "streamlit-image-coordinates-0.1.6.tar.gz", hash = "sha256:2327599727243a5ad9798e5767b624ab4d26985f606c6be435145eb1c4127d1d"}, + {file = "streamlit_image_coordinates-0.1.6-py3-none-any.whl", hash = "sha256:56b299c38c8c9aaa2fd724f12bc3b81017d01332a242152e10eeb63dffd50dd6"}, +] + +[package.dependencies] +jinja2 = "*" +streamlit = ">=1.2" + +[[package]] +name = "streamlit-keyup" +version = "0.2.3" +description = "Text input that renders on keyup" +optional = false +python-versions = ">=3.7" +files = [ + {file = "streamlit-keyup-0.2.3.tar.gz", hash = "sha256:7b7c4fa222dd82d1a479a05683833b2959c62b2dc243ffe0a769f4b79ac5b62c"}, + {file = "streamlit_keyup-0.2.3-py3-none-any.whl", hash = "sha256:e3fe87f69b5f800d29b5fe850a064532c2af0a8b1ef34047b9d33ff6c43feaf2"}, +] + +[package.dependencies] +jinja2 = "*" +streamlit = ">=1.2" + +[[package]] +name = "streamlit-toggle-switch" +version = "1.0.2" +description = "Creates a customizable toggle" +optional = false +python-versions = ">=3.6" +files = [ + {file = "streamlit_toggle_switch-1.0.2-py3-none-any.whl", hash = "sha256:0081212d80d178bda337acf2432425e2016d757f57834b18645d4c5b928d4c0f"}, + {file = "streamlit_toggle_switch-1.0.2.tar.gz", hash = "sha256:991b103cd3448b0f6507f8051777b996a17b4630956d5b6fa13344175b20e572"}, +] + +[package.dependencies] +streamlit = ">=0.63" + +[[package]] +name = "streamlit-vertical-slider" +version = "2.5.5" +description = "Creates a customizable vertical slider" +optional = false +python-versions = ">=3.8" +files = [ + {file = "streamlit_vertical_slider-2.5.5-py3-none-any.whl", hash = "sha256:8182e861444fcd69e05c05e7109a636d459560c249f1addf78b58e525a719cb6"}, + {file = "streamlit_vertical_slider-2.5.5.tar.gz", hash = "sha256:d6854cf81a606f5c021df2037d2c49036df2d03ce5082a5227a2acca8322ca74"}, +] + +[package.dependencies] +streamlit = ">=1.22.0" + +[[package]] +name = "tenacity" +version = "8.2.3" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, + {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, +] + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "toolz" +version = "0.12.1" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, + {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, +] + +[[package]] +name = "tornado" +version = "6.4" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">= 3.8" +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "watchdog" +version = "4.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, + {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, + {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, + {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, + {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, + {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, + {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, + {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "cb2a92d509f2bec9ddcea2640d01975c7bd1c4159399e87f58d041938b81722d" diff --git a/frontend/pyproject.toml b/frontend/pyproject.toml index 4356df4..8e750aa 100644 --- a/frontend/pyproject.toml +++ b/frontend/pyproject.toml @@ -5,3 +5,23 @@ version_scheme = "pep440" version = "0.1.0" update_changelog_on_bump = true major_version_zero = true + +[tool.poetry] +name = "frontend" +version = "0.1.0" +description = "basket recommendation front end" +authors = ["Judy "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" + + +[tool.poetry.group.dev.dependencies] +streamlit = "^1.32.2" +streamlit-antd-components = "^0.3.2" +streamlit-extras = "^0.4.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 9f494882b2c422aff237cc0906008666ad1a045b Mon Sep 17 00:00:00 2001 From: Judy Date: Mon, 18 Mar 2024 17:56:09 +0900 Subject: [PATCH 110/187] =?UTF-8?q?fix:=20=EC=9E=AC=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=8F=BC?= =?UTF-8?q?=EC=9D=B4=20=EB=B3=B4=EC=9D=B4=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#47?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/basket_login.py | 5 +---- frontend/common.py | 4 +--- frontend/newapp.py | 2 ++ 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/basket_login.py b/frontend/basket_login.py index e7998b4..010b654 100644 --- a/frontend/basket_login.py +++ b/frontend/basket_login.py @@ -39,9 +39,8 @@ def password_entered(): if status_code == 200: # 페이지 전환을 위해 - st.session_state.is_authenticated = True - st.session_state["password_correct"] = True + st.session_state.is_authenticated = True st.session_state.page_info = 'home2' st.session_state["token"] = { 'user_id': st.session_state['user_id'], @@ -52,9 +51,7 @@ def password_entered(): del st.session_state["password"] # Don't store the user_id or password. else: - st.session_state["password_correct"] = False - print(status_code) if status_code == 400: st.session_state.msg = "password incorrect" diff --git a/frontend/common.py b/frontend/common.py index 4e6fb39..b3c1f36 100644 --- a/frontend/common.py +++ b/frontend/common.py @@ -6,7 +6,6 @@ random_chars = lambda: ''.join(random.choices(string.ascii_letters + string.digits, k=5)) def init(): - print('init!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') st.session_state.is_authenticated = False st.session_state.page_info = 'home' st.session_state.url_prefix = 'http://localhost:8000' @@ -15,14 +14,13 @@ def init(): def set_logout_page(): st.session_state.is_authenticated = False st.session_state.page_info = 'home' + del st.session_state["password_correct"] def set_login_page(): st.session_state.page_info = 'login' def set_signup_page(): st.session_state.page_info = 'signup' - print('signup_page') - print(st.session_state.page_info) def login_button(): cols = st.columns(2) diff --git a/frontend/newapp.py b/frontend/newapp.py index ef795b8..1f3f42a 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -47,3 +47,5 @@ def choose_food(): else: home2() +print(st.session_state.is_authenticated) +print(st.session_state.page_info) From 0d26deaab7f0f33015821c348c7ef7ca864a1149 Mon Sep 17 00:00:00 2001 From: Judy Date: Mon, 18 Mar 2024 18:10:25 +0900 Subject: [PATCH 111/187] =?UTF-8?q?feat:=20=EC=B6=94=EC=B2=9C=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9E=AC=EB=A3=8C=EB=B3=84=20=EA=B0=80=EA=B2=A9=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A3=BC=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/result_page.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/result_page.py b/frontend/result_page.py index 312a844..bc560b2 100644 --- a/frontend/result_page.py +++ b/frontend/result_page.py @@ -15,8 +15,8 @@ def display_ingredients_in_rows_of_four(ingredients): st.markdown(f'Your Image', unsafe_allow_html=True) with cols[1]: - st.write(ingredient['ingredient_name']) - st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) + st.write(ingredient['ingredient_name'], ingredient['ingredient_amount'], ingredient['ingredient_unit']) + st.write(round(ingredient['ingredient_price']), '원') with cols[-1]: st.link_button('구매', ingredient['market_url'], type='primary') @@ -95,7 +95,7 @@ def result_page(): display_ingredients_in_rows_of_four(data['ingredient_list']) total_price = sum([ingredient['ingredient_price'] for ingredient in data['ingredient_list']]) - st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) + st.markdown(f"
예상 총 금액: {round(total_price)} 원
", unsafe_allow_html=True) st.divider() From 891d89046dd23972f50c6168746a0e043f52f70e Mon Sep 17 00:00:00 2001 From: Judy Date: Mon, 18 Mar 2024 19:59:17 +0900 Subject: [PATCH 112/187] =?UTF-8?q?feat:=20=EC=A7=80=EA=B8=88=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EB=B0=9B=EA=B8=B0,=20=EC=83=88=EB=A1=9C=EC=9A=B4?= =?UTF-8?q?=20=EC=B6=94=EC=B2=9C=20=EB=B0=9B=EA=B8=B0=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=EC=9E=AC=EA=B5=AC=EC=84=B1=20&=20=EC=9E=AC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=EC=8B=9C=20=EC=9D=B4=EC=A0=84=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EA=B8=B0=EB=A1=9D=20=EC=B4=88=EA=B8=B0=ED=99=94=20?= =?UTF-8?q?#6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/basket_login.py | 2 ++ frontend/newapp.py | 5 ----- frontend/pages/recommendation.py | 13 ++++++------ frontend/recommendation.py | 34 +++++++++++++++++++++++++------- frontend/result_page.py | 13 ++++++++++-- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/frontend/basket_login.py b/frontend/basket_login.py index 010b654..8310674 100644 --- a/frontend/basket_login.py +++ b/frontend/basket_login.py @@ -39,6 +39,8 @@ def password_entered(): if status_code == 200: # 페이지 전환을 위해 + if 'recommendation_result' in st.session_state: + del st.session_state['recommendation_result'] st.session_state["password_correct"] = True st.session_state.is_authenticated = True st.session_state.page_info = 'home2' diff --git a/frontend/newapp.py b/frontend/newapp.py index 1f3f42a..f5f653c 100644 --- a/frontend/newapp.py +++ b/frontend/newapp.py @@ -6,7 +6,6 @@ from main import main_page from main_2 import main_page_2 from signinpage_2 import choose_food_page -from recommendation import recommendation_page def home(): page_header() @@ -24,10 +23,6 @@ def signup(): page_header() signup_container() -def recommendation(): - page_header() - recommendation_page() - def choose_food(): page_header() choose_food_page() diff --git a/frontend/pages/recommendation.py b/frontend/pages/recommendation.py index 68ec7b4..0bb3e93 100644 --- a/frontend/pages/recommendation.py +++ b/frontend/pages/recommendation.py @@ -1,14 +1,16 @@ import streamlit as st from common import init, page_header -from newapp import recommendation from result_page import result_page from main import welcome_container -#from recommendation import recommendation_page +from recommendation import recommendation_page + +def recommendation(): + page_header() + recommendation_page() if 'is_authenticated' not in st.session_state: init() - print('page_recommendation') st.session_state.page_info = 'recommendation' def back_to_home_container(): @@ -23,10 +25,9 @@ def back_to_home_container(): if not st.session_state.is_authenticated: page_header() back_to_home_container() - elif st.session_state.get('is_authenticated', False) and (st.session_state.get('page_info', '-') == 'result_page_1'): result_page() - +elif st.session_state.get('is_authenticated', False) and (st.session_state.get('page_info', '-') == 'result_page_2'): + result_page() else: recommendation() - diff --git a/frontend/recommendation.py b/frontend/recommendation.py index 5092663..7e68ba1 100644 --- a/frontend/recommendation.py +++ b/frontend/recommendation.py @@ -1,4 +1,21 @@ import streamlit as st +import requests + +def set_result_page_2(): + st.session_state['page_info'] = 'result_page_2' + +def post_recommendation(): + full_url = st.session_state.url_prefix + '/api/users/{user_id}/recommendations?price={price}' + formatted_url = full_url.format(user_id=st.session_state.token['user_id'], price=st.session_state.price) + + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {st.session_state.token["token"]}', + } + + data = requests.post(formatted_url, headers=headers) + st.session_state.recommendation_result = data.json() + st.session_state['page_info'] = 'result_page_1' def recommendation_page(): @@ -30,10 +47,13 @@ def handle_change(): with cols[2]: st.write("예산: ", st.session_state.price, '원') - - cols = st.columns(3) - - with cols[1]: - button2 = st.button("장바구니 추천받기", type="primary") - if button2: - st.session_state['page_info'] = 'result_page_1' + if 'recommendation_result' in st.session_state: + cols = st.columns([3,2,3,3]) + with cols[1]: + button1 = st.button("이전 추천보기", on_click=set_result_page_2) + with cols[2]: + button2 = st.button("장바구니 추천받기", type="primary", on_click=post_recommendation) + else: + cols = st.columns([2,1.5,2]) + with cols[1]: + button2 = st.button("장바구니 추천받기", type="primary", on_click=post_recommendation) diff --git a/frontend/result_page.py b/frontend/result_page.py index bc560b2..a9858ce 100644 --- a/frontend/result_page.py +++ b/frontend/result_page.py @@ -3,6 +3,9 @@ import streamlit as st import requests +def set_recommendation(): + st.session_state['page_info'] = 'recommendation' + def display_ingredients_in_rows_of_four(ingredients): for ingredient in ingredients: sub_container = st.container(border=True) @@ -76,7 +79,8 @@ def result_page(): # url = api_prefix + "users/{user_id}/previousrecommendation" # formatted_url = url.format(user_id=st.session_state.user) # data = get_response(formatted_url) - data = post_recommendation() + #data = post_recommendation() + data = st.session_state.recommendation_result # 페이지 구성 container = st.container(border=True) @@ -104,4 +108,9 @@ def result_page(): display_recipes_in_rows_of_four(data['recipe_list']) st.text("\n\n") - basket_feedback() + + st.divider() + st.markdown("

다른 예산으로도 추천받아 볼까요?

", unsafe_allow_html=True) + cols = st.columns([2, 3, 1]) + with cols[1]: + button2 = st.button("추천받으러 가기 >>", type="primary", on_click=set_recommendation) From 6fa8dabb397793bb67629436d62246843f4ac215 Mon Sep 17 00:00:00 2001 From: GangBean Date: Tue, 19 Mar 2024 10:49:37 +0900 Subject: [PATCH 113/187] =?UTF-8?q?rafactor:=20rerun=20=EC=95=88=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=ED=94=BC=EB=93=9C=EB=B0=B1=EC=9D=B4=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=EB=90=98=EC=96=B4=EC=84=9C=20rerun=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=A4=84=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/user_history.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/user_history.py b/frontend/user_history.py index 0206018..a565981 100644 --- a/frontend/user_history.py +++ b/frontend/user_history.py @@ -48,7 +48,6 @@ def patch_feedback(user_id, recipe_id, current_state): } response = requests.patch(url.format(user_id=st.session_state.token['user_id'], recipe_id=recipe_id), json=data) print(f'status code: {response.status_code}') - st.rerun() def show_feedback_button(recipe_id, user_feedback): From 118aacd55add7cc4df9dc97546969da46ee3e624 Mon Sep 17 00:00:00 2001 From: GangBean Date: Tue, 19 Mar 2024 11:44:57 +0900 Subject: [PATCH 114/187] =?UTF-8?q?fix:=20slider=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=B0=92=EA=B3=BC=20=EC=98=88=EC=82=B0=EC=9D=B4=20=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/recommendation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/recommendation.py b/frontend/recommendation.py index 7e68ba1..0736a4f 100644 --- a/frontend/recommendation.py +++ b/frontend/recommendation.py @@ -30,7 +30,7 @@ def recommendation_page(): cols = st.columns([1,5,1]) if 'price' not in st.session_state: - st.session_state.price = 10000 + st.session_state.price = 50000 def handle_change(): st.session_state.price = st.session_state.price_slider @@ -38,7 +38,7 @@ def handle_change(): with cols[1]: st.slider( - label='price', min_value=10000, max_value=1000000, value=50000, step=5000, + label='price', min_value=10000, max_value=200000, value=50000, step=5000, on_change=handle_change, key='price_slider' ) From c37033e538f62f34da80acea4ef10400b2fd0713 Mon Sep 17 00:00:00 2001 From: GangBean Date: Wed, 20 Mar 2024 19:18:55 +0900 Subject: [PATCH 115/187] =?UTF-8?q?chore:=20poetry=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/poetry.lock | 275 ++++++++++++++++++++++++++++++++++++++++ crawling/pyproject.toml | 17 +++ 2 files changed, 292 insertions(+) create mode 100644 crawling/poetry.lock create mode 100644 crawling/pyproject.toml diff --git a/crawling/poetry.lock b/crawling/poetry.lock new file mode 100644 index 0000000..63ed91c --- /dev/null +++ b/crawling/poetry.lock @@ -0,0 +1,275 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "pydantic" +version = "2.6.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, + {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pymongo" +version = "4.6.2" +description = "Python driver for MongoDB " +optional = false +python-versions = ">=3.7" +files = [ + {file = "pymongo-4.6.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7640d176ee5b0afec76a1bda3684995cb731b2af7fcfd7c7ef8dc271c5d689af"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:4e2129ec8f72806751b621470ac5d26aaa18fae4194796621508fa0e6068278a"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c43205e85cbcbdf03cff62ad8f50426dd9d20134a915cfb626d805bab89a1844"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:91ddf95cedca12f115fbc5f442b841e81197d85aa3cc30b82aee3635a5208af2"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:0fbdbf2fba1b4f5f1522e9f11e21c306e095b59a83340a69e908f8ed9b450070"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:097791d5a8d44e2444e0c8c4d6e14570ac11e22bcb833808885a5db081c3dc2a"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:e0b208ebec3b47ee78a5c836e2e885e8c1e10f8ffd101aaec3d63997a4bdcd04"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1849fd6f1917b4dc5dbf744b2f18e41e0538d08dd8e9ba9efa811c5149d665a3"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa0bbbfbd1f8ebbd5facaa10f9f333b20027b240af012748555148943616fdf3"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4522ad69a4ab0e1b46a8367d62ad3865b8cd54cf77518c157631dac1fdc97584"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397949a9cc85e4a1452f80b7f7f2175d557237177120954eff00bf79553e89d3"}, + {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d511db310f43222bc58d811037b176b4b88dc2b4617478c5ef01fea404f8601"}, + {file = "pymongo-4.6.2-cp310-cp310-win32.whl", hash = "sha256:991e406db5da4d89fb220a94d8caaf974ffe14ce6b095957bae9273c609784a0"}, + {file = "pymongo-4.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:94637941fe343000f728e28d3fe04f1f52aec6376b67b85583026ff8dab2a0e0"}, + {file = "pymongo-4.6.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84593447a5c5fe7a59ba86b72c2c89d813fbac71c07757acdf162fbfd5d005b9"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aebddb2ec2128d5fc2fe3aee6319afef8697e0374f8a1fcca3449d6f625e7b4"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f706c1a644ed33eaea91df0a8fb687ce572b53eeb4ff9b89270cb0247e5d0e1"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c422e6b08fa370ed9d8670c67e78d01f50d6517cec4522aa8627014dfa38b6"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d002ae456a15b1d790a78bb84f87af21af1cb716a63efb2c446ab6bcbbc48ca"}, + {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f86ba0c781b497a3c9c886765d7b6402a0e3ae079dd517365044c89cd7abb06"}, + {file = "pymongo-4.6.2-cp311-cp311-win32.whl", hash = "sha256:ac20dd0c7b42555837c86f5ea46505f35af20a08b9cf5770cd1834288d8bd1b4"}, + {file = "pymongo-4.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:e78af59fd0eb262c2a5f7c7d7e3b95e8596a75480d31087ca5f02f2d4c6acd19"}, + {file = "pymongo-4.6.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6125f73503407792c8b3f80165f8ab88a4e448d7d9234c762681a4d0b446fcb4"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba052446a14bd714ec83ca4e77d0d97904f33cd046d7bb60712a6be25eb31dbb"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b65433c90e07dc252b4a55dfd885ca0df94b1cf77c5b8709953ec1983aadc03"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2160d9c8cd20ce1f76a893f0daf7c0d38af093f36f1b5c9f3dcf3e08f7142814"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f251f287e6d42daa3654b686ce1fcb6d74bf13b3907c3ae25954978c70f2cd4"}, + {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d227a60b00925dd3aeae4675575af89c661a8e89a1f7d1677e57eba4a3693c"}, + {file = "pymongo-4.6.2-cp312-cp312-win32.whl", hash = "sha256:311794ef3ccae374aaef95792c36b0e5c06e8d5cf04a1bdb1b2bf14619ac881f"}, + {file = "pymongo-4.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:f673b64a0884edcc56073bda0b363428dc1bf4eb1b5e7d0b689f7ec6173edad6"}, + {file = "pymongo-4.6.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:fe010154dfa9e428bd2fb3e9325eff2216ab20a69ccbd6b5cac6785ca2989161"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1f5f4cd2969197e25b67e24d5b8aa2452d381861d2791d06c493eaa0b9c9fcfe"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c9519c9d341983f3a1bd19628fecb1d72a48d8666cf344549879f2e63f54463b"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c68bf4a399e37798f1b5aa4f6c02886188ef465f4ac0b305a607b7579413e366"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a509db602462eb736666989739215b4b7d8f4bb8ac31d0bffd4be9eae96c63ef"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:362a5adf6f3f938a8ff220a4c4aaa93e84ef932a409abecd837c617d17a5990f"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:ee30a9d4c27a88042d0636aca0275788af09cc237ae365cd6ebb34524bddb9cc"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:477914e13501bb1d4608339ee5bb618be056d2d0e7267727623516cfa902e652"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd343ca44982d480f1e39372c48e8e263fc6f32e9af2be456298f146a3db715"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3797e0a628534e07a36544d2bfa69e251a578c6d013e975e9e3ed2ac41f2d95"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97d81d357e1a2a248b3494d52ebc8bf15d223ee89d59ee63becc434e07438a24"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed694c0d1977cb54281cb808bc2b247c17fb64b678a6352d3b77eb678ebe1bd9"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ceaaff4b812ae368cf9774989dea81b9bbb71e5bed666feca6a9f3087c03e49"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7dd63f7c2b3727541f7f37d0fb78d9942eb12a866180fbeb898714420aad74e2"}, + {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e571434633f99a81e081738721bb38e697345281ed2f79c2f290f809ba3fbb2f"}, + {file = "pymongo-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:3e9f6e2f3da0a6af854a3e959a6962b5f8b43bbb8113cd0bff0421c5059b3106"}, + {file = "pymongo-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3a5280f496297537301e78bde250c96fadf4945e7b2c397d8bb8921861dd236d"}, + {file = "pymongo-4.6.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:5f6bcd2d012d82d25191a911a239fd05a8a72e8c5a7d81d056c0f3520cad14d1"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4fa30494601a6271a8b416554bd7cde7b2a848230f0ec03e3f08d84565b4bf8c"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bea62f03a50f363265a7a651b4e2a4429b4f138c1864b2d83d4bf6f9851994be"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b2d445f1cf147331947cc35ec10342f898329f29dd1947a3f8aeaf7e0e6878d1"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:5db133d6ec7a4f7fc7e2bd098e4df23d7ad949f7be47b27b515c9fb9301c61e4"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:9eec7140cf7513aa770ea51505d312000c7416626a828de24318fdcc9ac3214c"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:5379ca6fd325387a34cda440aec2bd031b5ef0b0aa2e23b4981945cff1dab84c"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:579508536113dbd4c56e4738955a18847e8a6c41bf3c0b4ab18b51d81a6b7be8"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bae553ca39ed52db099d76acd5e8566096064dc7614c34c9359bb239ec4081"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0257e0eebb50f242ca28a92ef195889a6ad03dcdde5bf1c7ab9f38b7e810801"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbafe3a1df21eeadb003c38fc02c1abf567648b6477ec50c4a3c042dca205371"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaecfafb407feb6f562c7f2f5b91f22bfacba6dd739116b1912788cff7124c4a"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e942945e9112075a84d2e2d6e0d0c98833cdcdfe48eb8952b917f996025c7ffa"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f7b98f8d2cf3eeebde738d080ae9b4276d7250912d9751046a9ac1efc9b1ce2"}, + {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8110b78fc4b37dced85081d56795ecbee6a7937966e918e05e33a3900e8ea07d"}, + {file = "pymongo-4.6.2-cp38-cp38-win32.whl", hash = "sha256:df813f0c2c02281720ccce225edf39dc37855bf72cdfde6f789a1d1cf32ffb4b"}, + {file = "pymongo-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:64ec3e2dcab9af61bdbfcb1dd863c70d1b0c220b8e8ac11df8b57f80ee0402b3"}, + {file = "pymongo-4.6.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bff601fbfcecd2166d9a2b70777c2985cb9689e2befb3278d91f7f93a0456cae"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f1febca6f79e91feafc572906871805bd9c271b6a2d98a8bb5499b6ace0befed"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d788cb5cc947d78934be26eef1623c78cec3729dc93a30c23f049b361aa6d835"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5c2f258489de12a65b81e1b803a531ee8cf633fa416ae84de65cd5f82d2ceb37"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:fb24abcd50501b25d33a074c1790a1389b6460d2509e4b240d03fd2e5c79f463"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:4d982c6db1da7cf3018183891883660ad085de97f21490d314385373f775915b"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:b2dd8c874927a27995f64a3b44c890e8a944c98dec1ba79eab50e07f1e3f801b"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:4993593de44c741d1e9f230f221fe623179f500765f9855936e4ff6f33571bad"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658f6c028edaeb02761ebcaca8d44d519c22594b2a51dcbc9bd2432aa93319e3"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68109c13176749fbbbbbdb94dd4a58dcc604db6ea43ee300b2602154aebdd55f"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:707d28a822b918acf941cff590affaddb42a5d640614d71367c8956623a80cbc"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f251db26c239aec2a4d57fbe869e0a27b7f6b5384ec6bf54aeb4a6a5e7408234"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57c05f2e310701fc17ae358caafd99b1830014e316f0242d13ab6c01db0ab1c2"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b575fbe6396bbf21e4d0e5fd2e3cdb656dc90c930b6c5532192e9a89814f72d"}, + {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca5877754f3fa6e4fe5aacf5c404575f04c2d9efc8d22ed39576ed9098d555c8"}, + {file = "pymongo-4.6.2-cp39-cp39-win32.whl", hash = "sha256:8caa73fb19070008e851a589b744aaa38edd1366e2487284c61158c77fdf72af"}, + {file = "pymongo-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:3e03c732cb64b96849310e1d8688fb70d75e2571385485bf2f1e7ad1d309fa53"}, + {file = "pymongo-4.6.2.tar.gz", hash = "sha256:ab7d01ac832a1663dad592ccbd92bb0f0775bc8f98a1923c5e1a7d7fead495af"}, +] + +[package.dependencies] +dnspython = ">=1.16.0,<3.0.0" + +[package.extras] +aws = ["pymongo-auth-aws (<2.0.0)"] +encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] +gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] +ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +test = ["pytest (>=7)"] +zstd = ["zstandard"] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "f4960d2d6de80981a37fcffa665b428cc50cc399fd19f010b812937bad6f4727" diff --git a/crawling/pyproject.toml b/crawling/pyproject.toml new file mode 100644 index 0000000..6d7ad2f --- /dev/null +++ b/crawling/pyproject.toml @@ -0,0 +1,17 @@ +[tool.poetry] +name = "crawling" +version = "0.1.0" +description = "" +authors = ["GangBean "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +pymongo = "^4.6.2" +python-dotenv = "^1.0.1" +pydantic = "^2.6.4" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 64e7bff09a74ac6344adefcbe0e4762b7b611744 Mon Sep 17 00:00:00 2001 From: GangBean Date: Wed, 20 Mar 2024 19:19:51 +0900 Subject: [PATCH 116/187] =?UTF-8?q?chore:=20crawling=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=84=A4=EC=A0=95=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 crawling/__init__.py diff --git a/crawling/__init__.py b/crawling/__init__.py new file mode 100644 index 0000000..e69de29 From 584d59afeb508f201a04a141973775f638df44aa Mon Sep 17 00:00:00 2001 From: GangBean Date: Wed, 20 Mar 2024 19:20:33 +0900 Subject: [PATCH 117/187] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EB=AA=A8=EB=93=88=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/database/__init__.py | 0 crawling/database/data_source.py | 64 ++++++++++++++++++++++++++++++++ crawling/database/dev.env | 4 ++ crawling/database/prod.env | 4 ++ crawling/exception/__init__.py | 0 crawling/exception/database.py | 11 ++++++ 6 files changed, 83 insertions(+) create mode 100644 crawling/database/__init__.py create mode 100644 crawling/database/data_source.py create mode 100644 crawling/database/dev.env create mode 100644 crawling/database/prod.env create mode 100644 crawling/exception/__init__.py create mode 100644 crawling/exception/database.py diff --git a/crawling/database/__init__.py b/crawling/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crawling/database/data_source.py b/crawling/database/data_source.py new file mode 100644 index 0000000..0aafb69 --- /dev/null +++ b/crawling/database/data_source.py @@ -0,0 +1,64 @@ +import os + +from pymongo import MongoClient +from pymongo.collection import Collection +from pymongo.database import Database +from dotenv import load_dotenv +from pydantic import BaseModel +from typing import Optional + +from exception.database import ( + DatabaseNotFoundException, CollectionNotFoundException +) + +def env_file_of(env: str) -> str: + return os.path.join(os.path.dirname(os.path.abspath(__file__)), f"{env}.env") + +class DataSource(BaseModel): + host: str + port: str + database_name: str + client: Optional[object] = None + + def database(self) -> Database: + if self.client is None: + self._make_connection() + + self._validate_database() + return self.client[self.database_name] + + def collection_with_name_as(self, collection_name: str) -> Collection: + if self.client is None: + self._make_connection() + + self._validate_collection(collection_name) + return self.client[self.database_name][collection_name] + + def _make_connection(self) -> None: + url = f"mongodb://{self.host}:{self.port}/" + self.client = MongoClient(url) + + def _validate_database(self) -> None: + database_names = self.client.list_database_names() + if self.database_name not in database_names: + raise DatabaseNotFoundException(f"해당하는 데이터베이스가 존재하지 않습니다: {self.database_name}") + + def _validate_collection(self, collection_name) -> None: + collection_names = self.client[self.database_name].list_collection_names() + if collection_name not in collection_names: + raise CollectionNotFoundException(f"해당하는 컬렉션이 존재하지 않습니다: {collection_name}") + + +env_name = os.getenv('ENV', 'dev') +file_path = env_file_of(env_name) +assert os.path.exists(file_path), f"파일이 존재하지 않습니다: {file_path}" + +load_dotenv(file_path) + +data_env = { + 'host': os.getenv('HOST'), + 'port': os.getenv('PORT'), + 'database_name': os.getenv('DATABASE'), +} + +data_source = DataSource(**data_env) diff --git a/crawling/database/dev.env b/crawling/database/dev.env new file mode 100644 index 0000000..21fabab --- /dev/null +++ b/crawling/database/dev.env @@ -0,0 +1,4 @@ +ENV=dev +HOST=localhost +PORT=27017 +DATABASE=dev diff --git a/crawling/database/prod.env b/crawling/database/prod.env new file mode 100644 index 0000000..8f4c281 --- /dev/null +++ b/crawling/database/prod.env @@ -0,0 +1,4 @@ +ENV=prod +HOST=10.0.7.6 +PORT=27017 +DATABASE=prod diff --git a/crawling/exception/__init__.py b/crawling/exception/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crawling/exception/database.py b/crawling/exception/database.py new file mode 100644 index 0000000..11b787b --- /dev/null +++ b/crawling/exception/database.py @@ -0,0 +1,11 @@ +class DatabaseException(Exception): + def __init__(self, message): + super().__init__(message) + +class DatabaseNotFoundException(DatabaseException): + def __init__(self, message): + super().__init__(message) + +class CollectionNotFoundException(DatabaseException): + def __init__(self, message): + super().__init__(message) \ No newline at end of file From dbb936831e83bc5e10c9d36d8bdd48a554bbe8e1 Mon Sep 17 00:00:00 2001 From: GangBean Date: Wed, 20 Mar 2024 19:23:15 +0900 Subject: [PATCH 118/187] =?UTF-8?q?chore:=20pandas=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/poetry.lock | 167 +++++++++++++++++++++++++++++++++++++++- crawling/pyproject.toml | 1 + 2 files changed, 167 insertions(+), 1 deletion(-) diff --git a/crawling/poetry.lock b/crawling/poetry.lock index 63ed91c..5f0c1d9 100644 --- a/crawling/poetry.lock +++ b/crawling/poetry.lock @@ -31,6 +31,124 @@ idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "pandas" +version = "2.2.1" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"}, + {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"}, + {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"}, + {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"}, + {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"}, + {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"}, + {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"}, + {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"}, + {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"}, + {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"}, + {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"}, + {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"}, + {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"}, + {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"}, + {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"}, + {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"}, + {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"}, + {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + [[package]] name = "pydantic" version = "2.6.4" @@ -244,6 +362,20 @@ snappy = ["python-snappy"] test = ["pytest (>=7)"] zstd = ["zstandard"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -258,6 +390,28 @@ files = [ [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "typing-extensions" version = "4.10.0" @@ -269,7 +423,18 @@ files = [ {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "f4960d2d6de80981a37fcffa665b428cc50cc399fd19f010b812937bad6f4727" +content-hash = "6ff94ce7ca100bffc052ba32976dfd342646ea4cb68b519ba5d68b5760570554" diff --git a/crawling/pyproject.toml b/crawling/pyproject.toml index 6d7ad2f..d079f4b 100644 --- a/crawling/pyproject.toml +++ b/crawling/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.10" pymongo = "^4.6.2" python-dotenv = "^1.0.1" pydantic = "^2.6.4" +pandas = "^2.2.1" [build-system] From 4ad9c3877d5bff61c7b75d07c2cb1c8e4e76275e Mon Sep 17 00:00:00 2001 From: GangBean Date: Wed, 20 Mar 2024 22:02:25 +0900 Subject: [PATCH 119/187] =?UTF-8?q?chore:=20openai=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/poetry.lock | 192 +++++++++++++++++++++++++++++++++++++++- crawling/pyproject.toml | 1 + 2 files changed, 192 insertions(+), 1 deletion(-) diff --git a/crawling/poetry.lock b/crawling/poetry.lock index 5f0c1d9..51595e6 100644 --- a/crawling/poetry.lock +++ b/crawling/poetry.lock @@ -11,6 +11,61 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "distro" +version = "1.9.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, +] + [[package]] name = "dnspython" version = "2.6.1" @@ -31,6 +86,87 @@ idna = ["idna (>=3.6)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.4" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.25.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + [[package]] name = "numpy" version = "1.26.4" @@ -76,6 +212,29 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "openai" +version = "1.14.2" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "openai-1.14.2-py3-none-any.whl", hash = "sha256:a48b3c4d635b603952189ac5a0c0c9b06c025b80eb2900396939f02bb2104ac3"}, + {file = "openai-1.14.2.tar.gz", hash = "sha256:e5642f7c02cf21994b08477d7bb2c1e46d8f335d72c26f0396c5f89b15b5b153"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.7,<5" + +[package.extras] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] + [[package]] name = "pandas" version = "2.2.1" @@ -412,6 +571,37 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "tqdm" +version = "4.66.2" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + [[package]] name = "typing-extensions" version = "4.10.0" @@ -437,4 +627,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "6ff94ce7ca100bffc052ba32976dfd342646ea4cb68b519ba5d68b5760570554" +content-hash = "68f13cced23d1da044570007616c09588b0a65db04eb0214091e60b9201a245d" diff --git a/crawling/pyproject.toml b/crawling/pyproject.toml index d079f4b..e6c4cce 100644 --- a/crawling/pyproject.toml +++ b/crawling/pyproject.toml @@ -11,6 +11,7 @@ pymongo = "^4.6.2" python-dotenv = "^1.0.1" pydantic = "^2.6.4" pandas = "^2.2.1" +openai = "^1.14.2" [build-system] From b06a765103bb609baa683b90d9fe054662bccf3d Mon Sep 17 00:00:00 2001 From: GangBean Date: Thu, 21 Mar 2024 09:23:16 +0900 Subject: [PATCH 120/187] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/preprocess.py | 415 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 crawling/preprocess.py diff --git a/crawling/preprocess.py b/crawling/preprocess.py new file mode 100644 index 0000000..5524e6b --- /dev/null +++ b/crawling/preprocess.py @@ -0,0 +1,415 @@ +import time, sys +import multiprocessing +import concurrent.futures +import pandas as pd + +from datetime import datetime as dt +from pydantic import BaseModel +from pymongo.collection import Collection +from openai import OpenAI +from tqdm import tqdm + +from database.data_source import data_source +from database.data_source import data_source + +class Recipe(BaseModel): + recipe_id: str + recipe_title: str + recipe_name: str + author_id: str + author_name: str + recipe_method: str + recipe_status: str + recipe_kind: str + time_taken: str + difficulty: str + recipe_url: str + date_published: str + img_url: str + ingredients: str + review_count: int + photo_review_count: int + comment_count: int + portion: str + +class Preprocess: + MAIN_CATEGORIES = set([ + '주재료', + '재료', + ]) + + def __init__(self, data_file: str, api_key: str='AZC391KuQY9F1kWzQQntTU7fUoqffp4e'): + self.data: pd.DataFrame = self._na_replaced(pd.read_csv(data_file)) + self.recipe_repository: Collection = data_source.collection_with_name_as('recipes') + self.ingredient_repository: Collection = data_source.collection_with_name_as('ingredients') + self.llm_client: OpenAI = OpenAI( + api_key=api_key, + base_url="https://api.upstage.ai/v1/solar" + ) + self.run_time = dt.strftime(dt.now(), '%Y%m%d%H%M%S') + + def _na_replaced(self, df: pd.DataFrame): + ''' + recipe_id int64 recipe_id 0 + recipe_title object recipe_title 44 + recipe_name object recipe_name 119 + author_id object author_id 44 + author_name object author_name 1618 + recipe_method object recipe_method 4505 + recipe_status object recipe_status 5544 + recipe_kind object recipe_kind 4505 + time_taken object time_taken 18521 + difficulty object difficulty 2390 + recipe_url object recipe_url 0 + portion object portion 6911 + datePublished object datePublished 53 + food_img_url object food_img_url 266 + ingredient object ingredient 0 + reviews int64 reviews 0 + photo_reviews int64 photo_reviews 0 + comments int64 comments 0 + ''' + default_value = { + 'recipe_id': '9' * 7, + 'recipe_title': '맛있는 요리', + 'recipe_name': '맛있는 요리', + 'author_id': '9' * 7, + 'author_name': '작자미상', + 'recipe_method': '모름', + 'recipe_status': '모름', + 'recipe_kind': '모름', + 'time_taken': '모름', + 'difficulty': '초급', + 'recipe_url': 'https://www.10000recipe.com/index.html', + 'portion' : '1인분', + 'datePublished': dt.strftime(dt.now(), '%y%m%d%H%M%S'), + 'food_img_url': 'https://media.istockphoto.com/id/1283988510/ko/%EB%B2%A1%ED%84%B0/%EC%9D%BC%EB%B3%B8%EC%8B%9D-%EB%B0%A5.jpg?s=612x612&w=0&k=20&c=wr2rxOgUQbpaPBEC6PHqjS7B2b5_WQGcs5ujZERhX3U=', + 'ingredient': "{'주재료': {'name': '밥', 'amount': '1공기'}}", + 'reviews': 0, + 'photo_reviews': 0, + 'comments': 0 + } + df.fillna(default_value, inplace=True) + return df + + def multi_thread_run(self): + # with multiprocessing.Pool(processes=8) as pool: + + # # with tqdm(total=len(self.data)) as pbar: + # # for result in pool.map(self.run, self.data): + # # pbar.update() + # result = pool.imap(self.run, self.data) + # print(result.next()) + + # pool.close() + # pool.join() + + # with concurrent.futures.ThreadPoolExecutor() as executor: + # tqdm(executor.map(self.run, self.data), total=len(self.data)) + + with concurrent.futures.ThreadPoolExecutor() as executor: + num_groups = 8 + group_size = len(self.data) // num_groups + grouped_dfs = [self.data.iloc[i*group_size:(i+1)*group_size] for i in range(num_groups)] + + # 각 항목을 멀티 스레드로 처리하고 결과를 수집 + futures = [executor.submit(self.run, df) for df in grouped_dfs] + + # tqdm을 사용하여 진행 상황 모니터링 + with tqdm(total=len(self.data)) as pbar: + for future in concurrent.futures.as_completed(futures): + # 결과를 얻어와서 처리 + result = future.result() + # tqdm을 사용하여 진행 상황 업데이트 + pbar.update() + + def multi_processing_run(self): + with multiprocessing.Pool(processes=8) as pool: + result = pool.imap(self.run, self.data) + + + def run(self, data): + # 1. 데이터 로드 + # 2.1. recipe 단위 인스턴스 생성 + # 'recipe_id', 'recipe_title', 'recipe_name', 'author_id', 'author_name', + # 'recipe_method', 'recipe_status', 'recipe_kind', 'time_taken', + # 'difficulty', 'recipe_url', 'portion', 'datePublished', 'food_img_url', + # 'ingredient', 'reviews', 'photo_reviews', 'comments' + + ingredients_count = 0 + + for idx, row in tqdm(data.iterrows(), total = data.shape[0]): + if self.recipe_repository.find_one({'recipe_name': row['recipe_title']}): + continue + + try: + recipe: Recipe = Recipe( + recipe_id=str(row['recipe_id']), + recipe_title=row['recipe_title'], + recipe_name=row['recipe_name'], + author_id=str(row['author_id']), + author_name=row['author_name'], + recipe_method=row['recipe_method'], + recipe_status=row['recipe_status'], + recipe_kind=row['recipe_kind'], + time_taken=row['time_taken'], + difficulty=row['difficulty'], + recipe_url=row['recipe_url'], + portion=row['portion'], + date_published=str(row['datePublished']), + img_url=row['food_img_url'], + ingredients=row['ingredient'], + review_count=row['reviews'], + photo_review_count=row['photo_reviews'], + comment_count=row['comments'] + ) + + # 2.2. ingredients name 추출 + recipe.ingredients = Preprocess.dict_ingredients(recipe.ingredients) # dict로 변경 + # print('Dict', recipe.ingredients) + + # 2.2.1. 주재료 필터링 + recipe.ingredients = Preprocess.main_filtered(recipe.ingredients) + # print('Filter', recipe.ingredients) + + # 3. ingredients 저장 및 id 리스트 반환 + recipe.ingredients = self.inserted_ingredient_documents(recipe.ingredients) + # print('ID', recipe.ingredients) + + ingredients_count += len(recipe.ingredients) # name 업데이트 위함 + + # 4. recipes 저장 + self.insert_recipe(recipe) + + except KeyboardInterrupt: + raise KeyboardInterrupt + except: + pass + + self.ingredients_count = ingredients_count + + + def rename_ingredient_names(self, batch_size: int=500) -> int: + # batch_size개 ingredients 조회 + skip = 0 + total_iter_count = self.ingredients_count // batch_size + 1 + modified_count = 0 + + for _ in tqdm(range(total_iter_count)): + # batch_size개씩 문서를 조회하여 가져옴 + batch_ingredients = self.recipe_repository.find({}, {'name':1}).skip(skip).limit(batch_size) + + # name만 반환 + ingredient_names = [ingredient['name'] for ingredient in batch_ingredients] + + # ingredients name solar 변환 + ingredient_names = self.llm_parsed_ingredients(ingredient_names) + + # name 내 space 제거 + ingredient_names = Preprocess.without_space(ingredient_names) + + # 기존 이름 업데이트 + for name in ingredient_names: + for old_name, new_name in name.items(): + result = self.ingredient_repository.update_many({'name': old_name}, {'$set': {'name': new_name}}) + modified_count += result.modified_count + + # 다음 조회를 위해 skip 값을 업데이트 + skip += batch_size + + return modified_count + + + @staticmethod + def without_space(ingredients: list[dict]): + for ingredient in ingredients: + ingredient['name'] = ingredient['name'].replace(' ', '') + return ingredients + + @staticmethod + def dict_ingredients(ingredient: str) -> dict: + try: + return eval(ingredient) + except: + return {} + + @staticmethod + def main_filtered(info: dict) -> list[dict]: + ret = list() + # ingredient={ + # '재료': [{'name': '또띠아', 'amount': '1장'}, {'name': '모짜렐라치즈', 'amount': '마음껏'}, {'name': '꿀', 'amount': '마음껏'}, {'name': '다진마늘', 'amount': '1스푼'}, {'name': '버터', 'amount': '1스푼'}] + # } + for category, ingredients in info.items(): + if category in Preprocess.MAIN_CATEGORIES: # 메인 카테고리에 포함되면 계속 추가 + for ingredient in ingredients: + ret.append(ingredient) + continue + if len(ret) == 0: # 메인 카테고리에 해당하는 대상이 없으면 첫번째 카테고리만 포함시킴 + for ingredient in ingredients: + ret.append(ingredient) + break + return ret + + def _names_of(ingredients: list[dict]) -> list[str]: + return [ingredient['name'] for ingredient in ingredients] + + def _llm_stream(self, ingredient_names: list[str]): + try: + return self.llm_client.chat.completions.create( + model="solar-1-mini-chat", + messages=[ + { + "role": "system", + "content": "사용자가 한국 이커머스 사이트에서 식재료를 검색하여 구매하려고 해. 사용자가 입력하는 식재료명은 직접 검색어로 입력하였을 때에 식재료와 잘 매칭되지 않을 수 있어서, 검색이 가능한 형태로 변형하고 싶어. 검색 가능한 키워드로 바꿔주는 파이썬 딕셔너리를 반환해줘. 딕셔너리의 형태는 {<기존식재료명1>: <변형할 식재료명1>, <기존식재료명2>: <변형할 식재료명2>, ...} 으로 부탁해. 식재료명은 모두 한국어로 해주고, 변형이 어려운 식재료의 경우 변형할 식재료 명을 None으로 입력해도 좋아" + }, + { + "role": "user", + "content": f"{ingredient_names}" + } + ], + stream=True, + temperature=0.2, + ) + except: + return None + + def llm_parsed_ingredients(self, ingredients: list[dict]) -> list[dict]: + # 재료의 이름 모음 + ingredient_names = Preprocess._names_of(ingredients) + + # solar stream 생성 + stream = Preprocess._llm_stream(ingredient_names) + + retry_count = 0 + while stream is None: + if retry_count == 10: + raise ValueError('재시도 횟수 초과') + time.sleep(2) + stream = Preprocess._llm_stream(ingredient_names) + print(f"재시도 {retry_count}") + retry_count += 1 + + # 결과 파싱 + llm_dict = Preprocess._parsed_llm_output(stream) + + # dict 형태로 변환: 에러시 오류.. + try: + llm_dict = eval(llm_dict) + except: + with open(f'llm_output/error_.txt', 'a') as file: + file.write(str({ + 'input': ingredients, + 'output': llm_dict + })) + + # 생성된 매핑 딕셔너리로 기존 dict name replace + replaced_ingredients = Preprocess._replaced_name_ingredients(ingredients, llm_dict) + + return replaced_ingredients + + @staticmethod + def _replaced_name_ingredients(ingredients: list[dict], llm_dict: dict): + for ingredient in ingredients: + if ingredient['name'] not in llm_dict: # 이름이 존재하지 않으면 그냥 넘김 + continue + + replaced_name = llm_dict[ingredient['name']] + if replaced_name is None: # 결과가 None 이면 그냥 넘김 + continue + + ingredient['name'] = replaced_name + + return ingredients + + @staticmethod + def _parsed_llm_output(stream) -> str: + output = [] + for chunk in stream: + if chunk.choices[0].delta.content is not None: + content = chunk.choices[0].delta.content + # print(content, end="") + output.append(content) + + # print("\n-----------") + output = "".join(output) + # print(output) + start_idx, end_idx = output.find('{'), output.find('}') + output = output[start_idx:end_idx+1] + + with open(f'llm_output/_.txt', 'a') as file: + file.write(output) + + return output + + @staticmethod + def split_value_and_unit(input_string): + idx = -1 + reversed_string = input_string[::-1] + for i, c in enumerate(reversed_string): + if c.isdigit(): + idx = i + break + + idx = len(input_string)-idx + value = input_string[:idx] + unit = input_string[idx:] + + return value if (len(value) > 0) else '1', unit if (len(unit) > 0) else '개' + + def inserted_ingredient_documents(self, ingredient_names: list[dict]) -> list[str]: + ingredient_ids = list() + for ingredient_name in ingredient_names: + ingredient_ids.append(self._ingredient_id(ingredient_name)) + # ingredients id 리스트로 변환 + return ingredient_ids + + def _ingredient_id(self, ingredient: dict) -> str: + # ingredients collection search by name + # print(ingredient) + id = self.ingredient_repository.find_one({ + 'name': ingredient['name'] + }) + if id: + return str(id['_id']) + + # ingredients collection insert + return str( + self.ingredient_repository.insert_one({ + 'name': ingredient['name'], + 'amount': Preprocess._parsed_amount(ingredient['amount']) + }).inserted_id) + + @staticmethod + def _parsed_amount(amount: str) -> dict: + if amount is None: + return { + 'value': '1', + 'unit': '개' + } + + value, unit = Preprocess.split_value_and_unit(amount.strip()) + return { + 'value': value, + 'unit': unit + } + + def insert_recipe(self, recipe: Recipe) -> None: + self.recipe_repository.insert_one({ + 'food_name': recipe.recipe_name, + 'recipe_name': recipe.recipe_title, + 'ingredients': recipe.ingredients, + 'time_taken': recipe.time_taken, + 'difficulty': recipe.difficulty, + 'recipe_url': recipe.recipe_url, + 'portion': recipe.portion, + 'recipe_img_url': recipe.img_url, + }) + + +if __name__ == '__main__': + + count = sys.argv[-1] + + preprocess = Preprocess(f'recipe_data_merged_{count}.csv') + + preprocess.run() \ No newline at end of file From b2c5331316e006471b5d90a4d55b337906f41c0e Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Thu, 21 Mar 2024 10:31:57 +0900 Subject: [PATCH 121/187] =?UTF-8?q?feat:=20=EB=B0=B0=EC=B9=98=20=EC=84=9C?= =?UTF-8?q?=EB=B9=99=20=EC=BD=94=EB=93=9C=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?#31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airflow/dags/batch_inference.py | 93 +++++++++++++++++++++++++++++++ airflow/dags/db_config.py | 2 + airflow/dags/recbole_inference.py | 80 ++++++++++++++++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 airflow/dags/batch_inference.py create mode 100644 airflow/dags/db_config.py create mode 100644 airflow/dags/recbole_inference.py diff --git a/airflow/dags/batch_inference.py b/airflow/dags/batch_inference.py new file mode 100644 index 0000000..35838fd --- /dev/null +++ b/airflow/dags/batch_inference.py @@ -0,0 +1,93 @@ +from pymongo import MongoClient + +from datetime import datetime as dt +from datetime import timedelta + +from airflow import DAG +from airflow.utils.dates import days_ago +from airflow.operators.bash import BashOperator +from airflow.operators.python import PythonOperator +from recbole_inference import inference +from db_config import db_host, db_port + +def get_active_users(**context): + client = MongoClient(host=db_host, port=db_port) + db = client.dev + active_users = [user['login_id'] for user in db['users'].find()] + print(active_users) + + user_ids = ['76017883', '94541740'] + context["ti"].xcom_push(key='user_ids', value=user_ids) + +def batch_inference(**context): + # user_ids + user_ids = context["ti"].xcom_pull(key='user_ids') + + # 설정 파일과 모델 저장 경로 설정 + config_file_path = '/home/judy/train_model/recipe-dataset.yaml' # 설정 파일 경로 + model_file_path = '/home/judy/train_model/saved/MultiDAE-Mar-14-2024_23-15-20.pth' # 모델 파일 경로 + + recommended_items = inference( + user_ids=user_ids, + modelname='MultiDAE', + config_file=config_file_path, + model_file_path=model_file_path, + k=20) + + context["ti"].xcom_push(key='recommended_items', value=recommended_items) + +def save_results(**context): + user_ids = context["ti"].xcom_pull(key='user_ids') + recommended_items = context["ti"].xcom_pull(key='recommended_items') + + client = MongoClient(host=db_host, port=db_port) + db = client.dev + data = [{ + 'id': user_id, + 'recommended_item': recommended_item, + 'recommended_proba': recommended_proba, + 'date': dt.now() + } for user_id, recommended_item, recommended_proba in zip(user_ids, recommended_items['item_ids'], recommended_items['item_proba'])] + + db['model_recommendation_histories'].insert_many(data) + print('push data into db') + +with DAG( + dag_id="batch_inference", + description="batch inference of all active users using MultiDAE", + start_date=days_ago(5), # DAG 정의 기준 2일 전부터 시작합니다. + schedule_interval="0 2 * * *", # 매일 2시에 시작 + tags=["basket_recommendation", "inference"], + ) as dag: + + # get active user + t1 = PythonOperator( + task_id="get_active_users", + python_callable=get_active_users, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # inference + t2 = PythonOperator( + task_id="batch_inference", + python_callable=batch_inference, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # save results + t3 = PythonOperator( + task_id="save_results", + python_callable=save_results, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + t1 >> t2 >> t3 diff --git a/airflow/dags/db_config.py b/airflow/dags/db_config.py new file mode 100644 index 0000000..4851aa2 --- /dev/null +++ b/airflow/dags/db_config.py @@ -0,0 +1,2 @@ +db_host = '10.0.7.6' +db_port = 27017 diff --git a/airflow/dags/recbole_inference.py b/airflow/dags/recbole_inference.py new file mode 100644 index 0000000..40a2b98 --- /dev/null +++ b/airflow/dags/recbole_inference.py @@ -0,0 +1,80 @@ +from typing import List + +import torch + +from recbole.config import Config +from recbole.data import create_dataset, data_preparation +from recbole.model.general_recommender import MultiDAE # BPR +from recbole.trainer import Trainer +from recbole.utils import init_logger, init_seed +from recbole.data.interaction import Interaction + +def inference(user_ids: List[str], modelname: str, config_file: str, model_file_path: str, k: int=20): + + # 설정 파일 로드 + config = Config(model=modelname, dataset='recipe-dataset', config_file_list=[config_file]) + + # 데이터셋 생성 + dataset = create_dataset(config) + + # 데이터 분할 + train_data, valid_data, test_data = data_preparation(config, dataset) + + # 모델 초기화 + if modelname == 'MultiDAE': + model = MultiDAE(config, train_data.dataset).to(config['device']) + else: + raise ValueError(f'{modelname} Not Found. Train First') + + # 모델 파라미터 로드 + model.load_state_dict(torch.load(model_file_path)['state_dict']) + + # 추론 준비 + model.eval() + + # 사용자 ID에 대한 텐서 생성 + user_ids = [dataset.token2id(dataset.uid_field, user_id) for user_id in user_ids] + user_dict = { + dataset.uid_field: torch.tensor(user_ids, dtype=torch.int64).to(config['device']) + } + + # Interaction 객체 생성 + interaction = Interaction(user_dict) + + # 모델을 사용하여 추천 생성 + scores = model.full_sort_predict(interaction).view(-1, dataset.item_num) + probas = torch.sigmoid(scores) + + # 실제 인터렉션이 있는 위치를 매우 낮은 값으로 마스킹 + user_interactions = model.get_rating_matrix(interaction['user_id']) + masked_scores = probas.clone() + masked_scores[user_interactions >= 1] = -1e9 + + # 확률이 높은 아이템 20개 추출 + topk_proba, topk_item = torch.topk(masked_scores, k, dim=1) + + item_ids = [ + dataset.id2token(dataset.iid_field, item_token).tolist()\ + for item_token in topk_item.detach().cpu().numpy()] + item_proba = topk_proba.detach().cpu().numpy() + + print(item_ids) + print(item_proba) + + return {'item_ids': item_ids, 'item_proba': item_proba.tolist()} + +if __name__ == '__main__': + + # 설정 파일과 모델 저장 경로 설정 + config_file_path = '/home/judy/train_model/recipe-dataset.yaml' # 설정 파일 경로 + model_file_path = '/home/judy/train_model/saved/MultiDAE-Mar-14-2024_23-15-20.pth' # 모델 파일 경로 + + recommended_items = inference( + user_ids=['76017883', '94541740'], + modelname='MultiDAE', + config_file=config_file_path, + model_file_path=model_file_path, + k=20) + + print(recommended_items['item_ids']) + print(recommended_items['item_proba']) From 7c247b6321765e97e56b7cf1a435a1cc44a596a0 Mon Sep 17 00:00:00 2001 From: Judy <95452963+twndus@users.noreply.github.com> Date: Thu, 21 Mar 2024 12:59:48 +0900 Subject: [PATCH 122/187] Exp/model eval (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 1.크롤링 데이터 병합 #51 * feat: 2. 데이터 전처리 코드 업로드 #51 * feat: 학습, 평가 데이터 분할 코드 업로드 #51 * feat: Recbole 요구 조건에 맞춰 데이터셋 변환 코드 업로드 #51 --- ml/eval/1.merge-recipes-reviews.ipynb | 349 ++++++++++++++++++++++++ ml/eval/2.preprocessing.ipynb | 230 ++++++++++++++++ ml/eval/3.data-split.ipynb | 371 ++++++++++++++++++++++++++ ml/eval/4.make-iter.ipynb | 260 ++++++++++++++++++ 4 files changed, 1210 insertions(+) create mode 100644 ml/eval/1.merge-recipes-reviews.ipynb create mode 100644 ml/eval/2.preprocessing.ipynb create mode 100644 ml/eval/3.data-split.ipynb create mode 100644 ml/eval/4.make-iter.ipynb diff --git a/ml/eval/1.merge-recipes-reviews.ipynb b/ml/eval/1.merge-recipes-reviews.ipynb new file mode 100644 index 0000000..8a1c166 --- /dev/null +++ b/ml/eval/1.merge-recipes-reviews.ipynb @@ -0,0 +1,349 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1. 레시피 정보과 리뷰 정보를 병합하여 저장" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from datetime import datetime as dt\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "raw_data_dir = '/dev/shm/data/1.raw'" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((1429, 2), (221928, 5))" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recipe_df = pd.read_csv(os.path.join(raw_data_dir, 'recipes_full_240313.csv'))\n", + "review_df = pd.read_csv(os.path.join(raw_data_dir, 'reviews_full_240313.csv'))\n", + "\n", + "recipe_df.shape, review_df.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 중복 개체 제거" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "before drop duplicated users: 221928\n", + "after drop duplicated users: 128555\n" + ] + } + ], + "source": [ + "# 유저 중복 제거 - 리뷰\n", + "print(f'before drop duplicated users: {review_df.shape[0]}')\n", + "review_df = review_df.drop_duplicates(subset=['uid'])\n", + "print(f'after drop duplicated users: {review_df.shape[0]}')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "before drop duplicated users: 1429\n", + "after drop duplicated users: 1429\n" + ] + } + ], + "source": [ + "# 유저 중복 제거 - 레시피\n", + "print(f'before drop duplicated users: {recipe_df.shape[0]}')\n", + "review_df = review_df.drop_duplicates(subset=['uid'])\n", + "print(f'after drop duplicated users: {recipe_df.shape[0]}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "트랜젝션 데이터로 변환\n", + "\n", + "uid, user_name, itemid, rating, date" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# str -> dict\n", + "review_df['history'] = review_df['history'].apply(eval)\n", + "recipe_df['recipes'] = recipe_df['recipes'].apply(eval)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 128555/128555 [00:08<00:00, 14344.54it/s]\n" + ] + } + ], + "source": [ + "review_transactions = []\n", + "\n", + "for i, row in tqdm(review_df.iterrows(), total=review_df.shape[0]):\n", + "\tuid = row['uid']\n", + "\tfor recipe_sno, data in row['history'].items():\n", + "\t\trating = data['rating']\n", + "\t\tdatetime = data['datetime']\n", + "\t\treview_transactions.append([uid, recipe_sno, rating, datetime])\n", + "\n", + "review_transaction_df = pd.DataFrame(review_transactions, columns=['uid', 'recipe_sno', 'rating', 'datetime'])" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1429/1429 [00:00<00:00, 12992.78it/s]\n" + ] + } + ], + "source": [ + "recipe_transactions = []\n", + "\n", + "for i, row in tqdm(recipe_df.iterrows(), total=recipe_df.shape[0]):\n", + "\tuid = row['uid']\n", + "\tfor recipe_sno in row['recipes']:\n", + "\t\trecipe_transactions.append([uid, recipe_sno])\n", + "\n", + "recipe_transaction_df = pd.DataFrame(recipe_transactions, columns=['uid', 'recipe_sno'])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
0gomusin766834819NaNNaN
1gomusin766834466NaNNaN
2gomusin766834339NaNNaN
3gomusin766834128NaNNaN
4gomusin766834038NaNNaN
...............
4185537213997568918165.02021-09-28 12:28
4185547729634168698015.02023-08-05 22:28
4185552512100068918165.02022-11-02 20:15
4185563898666641642295.02023-11-06 19:35
4185574182573741642295.02019-01-25 21:43
\n", + "

436932 rows × 4 columns

\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "0 gomusin76 6834819 NaN NaN\n", + "1 gomusin76 6834466 NaN NaN\n", + "2 gomusin76 6834339 NaN NaN\n", + "3 gomusin76 6834128 NaN NaN\n", + "4 gomusin76 6834038 NaN NaN\n", + "... ... ... ... ...\n", + "418553 72139975 6891816 5.0 2021-09-28 12:28\n", + "418554 77296341 6869801 5.0 2023-08-05 22:28\n", + "418555 25121000 6891816 5.0 2022-11-02 20:15\n", + "418556 38986666 4164229 5.0 2023-11-06 19:35\n", + "418557 41825737 4164229 5.0 2019-01-25 21:43\n", + "\n", + "[436932 rows x 4 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_transaction_df = pd.concat([recipe_transaction_df, review_transaction_df], axis=0)\n", + "all_transaction_df.drop_duplicates()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "now = dt.now().strftime('%y%m%d')\n", + "all_transaction_df.to_csv(f'/dev/shm/data/2.merged/merged-data-{now}.csv', index=False)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "airflow", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ml/eval/2.preprocessing.ipynb b/ml/eval/2.preprocessing.ipynb new file mode 100644 index 0000000..9f03145 --- /dev/null +++ b/ml/eval/2.preprocessing.ipynb @@ -0,0 +1,230 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2. 데이터 전처리" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from datetime import datetime as dt\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "merged_data_dir = '/dev/shm/data/2.merged'" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "all_transaction_df shape: (450388, 4)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
2337869596884869574725.02023-09-08 19:50
2742053989756068685285.02021-12-14 23:32
3655583466614168681825.02017-06-14 21:21
1413243463911468698075.02021-10-20 13:57
2839501209808968780015.02019-08-13 11:26
\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "233786 95968848 6957472 5.0 2023-09-08 19:50\n", + "274205 39897560 6868528 5.0 2021-12-14 23:32\n", + "365558 34666141 6868182 5.0 2017-06-14 21:21\n", + "141324 34639114 6869807 5.0 2021-10-20 13:57\n", + "283950 12098089 6878001 5.0 2019-08-13 11:26" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_transaction_df = pd.read_csv(os.path.join(merged_data_dir, 'merged-data-240321.csv'))\n", + "print('all_transaction_df shape: ', all_transaction_df.shape)\n", + "all_transaction_df.sample(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5, 10개 이하의 상호작용만 있는 유저, 레시피 제거" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def calculate_sparsity(target_df, threshold):\n", + "\tdf = target_df.uid.value_counts()\n", + "\tuser_over_5 = (df[df >= threshold]).index\n", + "\tdf = target_df[target_df.uid.isin(user_over_5)]\n", + "\tlen_unique_user = df.uid.nunique()\n", + "\tlen_unique_recipe = df.recipe_sno.nunique()\n", + "\tsparsity = df.shape[0]/df.uid.nunique()/df.recipe_sno.nunique() * 100\n", + "\treturn len_unique_user, len_unique_recipe, sparsity, df" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(20588, 58920, 0.02282248466701514, (276847, 4))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len_unique_user, len_unique_recipe, sparsity, all_transaction_5_df = calculate_sparsity(all_transaction_df, 5)\n", + "len_unique_user, len_unique_recipe, sparsity, all_transaction_5_df.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(7992, 51317, 0.0478597446951014, (196285, 4))" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len_unique_user, len_unique_recipe, sparsity, all_transaction_10_df = calculate_sparsity(all_transaction_df, 10)\n", + "len_unique_user, len_unique_recipe, sparsity, all_transaction_10_df.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "now = dt.now().strftime('%y%m%d')\n", + "all_transaction_5_df.to_csv(f'/dev/shm/data/3.preprocessed/merged-data-over-5-{now}.csv', index=False)\n", + "all_transaction_10_df.to_csv(f'/dev/shm/data/3.preprocessed/merged-data-over-10-{now}.csv', index=False)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "airflow", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ml/eval/3.data-split.ipynb b/ml/eval/3.data-split.ipynb new file mode 100644 index 0000000..24689c5 --- /dev/null +++ b/ml/eval/3.data-split.ipynb @@ -0,0 +1,371 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 3. data split by user" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from datetime import datetime as dt\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "preprocessed_data_dir = '/dev/shm/data/3.preprocessed/'" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "all_transaction_5_df shape: (276847, 4)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
1089382791818168819905.02018-04-06 18:44
1773627873017168323255.02018-06-26 17:38
939527741292669009885.02023-12-08 11:05
1231775809997246072505.02017-08-25 15:11
1326391810539768472215.02023-01-05 10:03
\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "108938 27918181 6881990 5.0 2018-04-06 18:44\n", + "177362 78730171 6832325 5.0 2018-06-26 17:38\n", + "93952 77412926 6900988 5.0 2023-12-08 11:05\n", + "123177 58099972 4607250 5.0 2017-08-25 15:11\n", + "132639 18105397 6847221 5.0 2023-01-05 10:03" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_transaction_5_df = pd.read_csv(os.path.join(preprocessed_data_dir, 'merged-data-over-5-240321.csv'))\n", + "print('all_transaction_5_df shape: ', all_transaction_5_df.shape)\n", + "all_transaction_5_df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "all_transaction_10_df shape: (196285, 4)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
28889791155186929687NaNNaN
1187076775379768804625.02018-04-19 20:01
81459darkngelmk68685305.02019-11-12 06:27
16604kpmj16871702NaNNaN
775967783472668766935.02018-08-07 20:12
\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "28889 79115518 6929687 NaN NaN\n", + "118707 67753797 6880462 5.0 2018-04-19 20:01\n", + "81459 darkngelmk 6868530 5.0 2019-11-12 06:27\n", + "16604 kpmj1 6871702 NaN NaN\n", + "77596 77834726 6876693 5.0 2018-08-07 20:12" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_transaction_10_df = pd.read_csv(os.path.join(preprocessed_data_dir, 'merged-data-over-10-240321.csv'))\n", + "print('all_transaction_10_df shape: ', all_transaction_10_df.shape)\n", + "all_transaction_10_df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def split_by_user(df, train_ratio=.8):\n", + "\ttrain_ratio = .8\n", + "\ttrain_data = pd.DataFrame(columns=df.columns)\n", + "\ttest_data = pd.DataFrame(columns=df.columns)\n", + "\n", + "\tfor uid, user_df in tqdm(df.groupby('uid')):\n", + "\n", + "\t\t# 시간 순으로 정렬 (na의 경우 가장 오래된 데이터 취급)\n", + "\t\tsorted_user_df = user_df.sort_values(by='datetime', na_position='first')\n", + "\n", + "\t\t# 목표 분할 비율에 맞는 인덱스 찾기\n", + "\t\tsplit_point = int(len(sorted_user_df) * train_ratio)\n", + "\n", + "\t\t# 데이터 분할\n", + "\t\ttrain_data = pd.concat([train_data, sorted_user_df.iloc[:split_point]])\n", + "\t\ttest_data = pd.concat([test_data, sorted_user_df.iloc[split_point:]])\n", + "\t\n", + "\treturn train_data, test_data" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " 0%| | 0/20588 [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
1353687362919668667345.02019-03-10 11:43
191771chiy110168529345.02016-08-22 23:28
934925264845168732445.02020-05-24 20:17
683394015415968512725.02017-06-09 16:11
207261988877468748665.02019-05-29 11:14
\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "135368 73629196 6866734 5.0 2019-03-10 11:43\n", + "191771 chiy1101 6852934 5.0 2016-08-22 23:28\n", + "93492 52648451 6873244 5.0 2020-05-24 20:17\n", + "68339 40154159 6851272 5.0 2017-06-09 16:11\n", + "20726 19888774 6874866 5.0 2019-05-29 11:14" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_df = pd.read_csv(os.path.join(data_dir, 'train-data-over-5-240321.csv'))\n", + "print('train_df shape: ', train_df.shape)\n", + "train_df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "test_df shape: (63090, 4)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
194593931550969285945.02023-01-21 18:20
449168073565128004835.02022-04-09 18:29
233644554347468422585.02016-04-22 10:08
534439476240869486085.02024-01-27 02:08
45174808022736971235NaNNaN
\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "19459 39315509 6928594 5.0 2023-01-21 18:20\n", + "44916 80735651 2800483 5.0 2022-04-09 18:29\n", + "23364 45543474 6842258 5.0 2016-04-22 10:08\n", + "53443 94762408 6948608 5.0 2024-01-27 02:08\n", + "45174 80802273 6971235 NaN NaN" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_df = pd.read_csv(os.path.join(data_dir, 'test-data-over-5-240321.csv'))\n", + "print('test_df shape: ', test_df.shape)\n", + "test_df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "airflow", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From cd5a0a649841cc9775ee4ffbcc35702219892b39 Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Thu, 21 Mar 2024 17:16:45 +0900 Subject: [PATCH 123/187] =?UTF-8?q?feat:=20sequential=20recommendation=20?= =?UTF-8?q?=ED=95=99=EC=8A=B5=20=EC=BD=94=EB=93=9C=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20#52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/Sequential/eda-seq.ipynb | 451 ++++++++++++++++++++++++++++ ml/Sequential/seq-train-config.yaml | 51 ++++ ml/Sequential/seq-train.py | 9 + ml/eval/4.make-iter.ipynb | 236 ++++++++++----- 4 files changed, 676 insertions(+), 71 deletions(-) create mode 100644 ml/Sequential/eda-seq.ipynb create mode 100644 ml/Sequential/seq-train-config.yaml create mode 100644 ml/Sequential/seq-train.py diff --git a/ml/Sequential/eda-seq.ipynb b/ml/Sequential/eda-seq.ipynb new file mode 100644 index 0000000..aa98099 --- /dev/null +++ b/ml/Sequential/eda-seq.ipynb @@ -0,0 +1,451 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 시퀀셜 데이터의 길이 파악하기" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from datetime import datetime as dt\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "data_dir = '/dev/shm/data/3.preprocessed'" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(276847, 4)\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
398849648078868940965.02020-11-15 20:16
17188hskim41277018883NaNNaN
1933607592668969292935.02021-01-25 13:39
1823439119227234501465.02019-12-16 22:40
5016330060566879915NaNNaN
\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "39884 96480788 6894096 5.0 2020-11-15 20:16\n", + "17188 hskim4127 7018883 NaN NaN\n", + "193360 75926689 6929293 5.0 2021-01-25 13:39\n", + "182343 91192272 3450146 5.0 2019-12-16 22:40\n", + "5016 33006056 6879915 NaN NaN" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = pd.read_csv(os.path.join(data_dir, 'merged-data-over-5-240321.csv'))\n", + "print(df.shape)\n", + "df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
uidrecipe_snoratingdatetime
1783811091282168572785.02020-10-17 11:21
28152397441836977345NaNNaN
1941335827993369084985.02023-05-21 19:03
2632723720699868918165.02022-01-12 15:17
2677743236262668403105.02023-10-03 09:14
\n", + "
" + ], + "text/plain": [ + " uid recipe_sno rating datetime\n", + "178381 10912821 6857278 5.0 2020-10-17 11:21\n", + "28152 39744183 6977345 NaN NaN\n", + "194133 58279933 6908498 5.0 2023-05-21 19:03\n", + "263272 37206998 6891816 5.0 2022-01-12 15:17\n", + "267774 32362626 6840310 5.0 2023-10-03 09:14" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# sort by uid, datetime\n", + "sorted_df = df.sort_values(by=['uid', 'datetime'], na_position='first')\n", + "sorted_df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
recipe_sno
uid
24900330[6968044, 6950781, 6950376, 6949766, 6948855, ...
17536804[6912137, 6866453, 6938845, 6848377, 6858795]
48902769[6841018, 6865170, 6859263, 5419824, 6868963, ...
14209108[6854350, 6887218, 6831734, 6843865, 6896724, ...
58428023[6850895, 6852196, 6838648, 6836180, 6833703, ...
\n", + "
" + ], + "text/plain": [ + " recipe_sno\n", + "uid \n", + "24900330 [6968044, 6950781, 6950376, 6949766, 6948855, ...\n", + "17536804 [6912137, 6866453, 6938845, 6848377, 6858795]\n", + "48902769 [6841018, 6865170, 6859263, 5419824, 6868963, ...\n", + "14209108 [6854350, 6887218, 6831734, 6843865, 6896724, ...\n", + "58428023 [6850895, 6852196, 6838648, 6836180, 6833703, ..." + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# groupby seq\n", + "seq_df = sorted_df.groupby('uid').agg({'recipe_sno': list})\n", + "seq_df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk0AAAGxCAYAAAB/QoKnAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA9VUlEQVR4nO3dfVhVdb7//9dG3RtNARW5mxAxTUXxDo2oNE0CjWlycsrM0ooyO5B3HTXKDPXM4NGjZWV6mlKa72iaM2WlDop4n3iHouINpWI45cZKZasZKqzfHx3Wz52KS0PZ6PNxXeu6WOvz3p/1+Xwurs3rWnvthc0wDEMAAACokFdVDwAAAKA6IDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFtSs6gHcKMrKyvTdd9+pXr16stlsVT0cAABggWEYOnHihEJCQuTlVfG1JEJTJfnuu+8UGhpa1cMAAABX4dChQ7r11lsrrCE0VZJ69epJ+mXRfXx8qng0AADACpfLpdDQUPPveEUITZWk/CM5Hx8fQhMAANWMlVtruBEcAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhQs6oHAGsKCwv1ww8/VHq//v7+aty4caX3CwDAjYbQVA0UFhaqZctWOn36p0rvu3btOtq7dw/BCQCAyyA0VQM//PCDTp/+SdHPvC6f4CaV1q/r8EFtnDVOP/zwA6EJAIDLIDRVIz7BTdSgcYuqHgYAADclbgQHAACwgNAEAABgQZWGprS0NHXu3Fn16tVTQECAevfurfz8fLean3/+WUlJSWrYsKHq1q2rPn36qKioyK2msLBQCQkJqlOnjgICAjRy5EidO3fOrWbVqlXq2LGjHA6HmjVrpvT09AvGM336dDVp0kTe3t6Kjo7Wpk2bKn3OAACgeqrS0LR69WolJSVpw4YNyszM1NmzZxUXF6dTp06ZNcOHD9cXX3yhBQsWaPXq1fruu+/08MMPm+2lpaVKSEjQmTNntH79en344YdKT0/X2LFjzZqCggIlJCSoe/fuys3N1bBhw/Tss89q6dKlZs38+fM1YsQIvf7669q6davatWun+Ph4HTly5PosBgAA8Gg2wzCMqh5Eue+//14BAQFavXq1unbtquLiYjVq1Ehz587Vn/70J0nS3r171apVK2VnZ+vOO+/Uv/71L/3+97/Xd999p8DAQEnSzJkzNXr0aH3//fey2+0aPXq0Fi9erLy8PPNcjz32mI4fP66MjAxJUnR0tDp37qx33nlHklRWVqbQ0FC9+OKLevnlly87dpfLJV9fXxUXF8vHx6dS12Xr1q2KiorS/a/OrtQbwY8W5ivzz08rJydHHTt2rLR+AQCoLq7k77dH3dNUXFwsSWrQoIEkKScnR2fPnlVsbKxZ07JlSzVu3FjZ2dmSpOzsbEVGRpqBSZLi4+Plcrm0a9cus+b8Pspryvs4c+aMcnJy3Gq8vLwUGxtr1vxaSUmJXC6X2wYAAG5cHhOaysrKNGzYMN19991q06aNJMnpdMput8vPz8+tNjAwUE6n06w5PzCVt5e3VVTjcrl0+vRp/fDDDyotLb1oTXkfv5aWliZfX19zCw0NvbqJAwCAasFjQlNSUpLy8vI0b968qh6KJSkpKSouLja3Q4cOVfWQAADANeQRD7dMTk7WokWLtGbNGt16663m8aCgIJ05c0bHjx93u9pUVFSkoKAgs+bX33Ir/3bd+TW//sZdUVGRfHx8VLt2bdWoUUM1atS4aE15H7/mcDjkcDiubsIAAKDaqdIrTYZhKDk5WZ9++qlWrFih8PBwt/aoqCjVqlVLWVlZ5rH8/HwVFhYqJiZGkhQTE6OdO3e6fcstMzNTPj4+ioiIMGvO76O8prwPu92uqKgot5qysjJlZWWZNQAA4OZWpVeakpKSNHfuXH322WeqV6+eef+Qr6+vateuLV9fXyUmJmrEiBFq0KCBfHx89OKLLyomJkZ33nmnJCkuLk4RERF68sknNWnSJDmdTo0ZM0ZJSUnmlaDBgwfrnXfe0ahRo/TMM89oxYoV+vjjj7V48WJzLCNGjNDAgQPVqVMn3XHHHXrzzTd16tQpPf3009d/YQAAgMep0tA0Y8YMSVK3bt3cjs+ePVtPPfWUJOmNN96Ql5eX+vTpo5KSEsXHx+vdd981a2vUqKFFixbphRdeUExMjG655RYNHDhQ48ePN2vCw8O1ePFiDR8+XNOmTdOtt96q999/X/Hx8WZN37599f3332vs2LFyOp1q3769MjIyLrg5HAAA3Jw86jlN1RnPaQIAoPqpts9pAgAA8FSEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMCCKg1Na9as0YMPPqiQkBDZbDYtXLjQrd1ms110mzx5slnTpEmTC9onTpzo1s+OHTvUpUsXeXt7KzQ0VJMmTbpgLAsWLFDLli3l7e2tyMhILVmy5JrMGQAAVE9VGppOnTqldu3aafr06RdtP3z4sNs2a9Ys2Ww29enTx61u/PjxbnUvvvii2eZyuRQXF6ewsDDl5ORo8uTJSk1N1XvvvWfWrF+/Xv369VNiYqK2bdum3r17q3fv3srLy7s2EwcAANVOzao8ea9evdSrV69LtgcFBbntf/bZZ+revbuaNm3qdrxevXoX1JabM2eOzpw5o1mzZslut6t169bKzc3V1KlTNWjQIEnStGnT1LNnT40cOVKSNGHCBGVmZuqdd97RzJkzf8sUAQDADaLa3NNUVFSkxYsXKzEx8YK2iRMnqmHDhurQoYMmT56sc+fOmW3Z2dnq2rWr7Ha7eSw+Pl75+fk6duyYWRMbG+vWZ3x8vLKzsy85npKSErlcLrcNAADcuKr0StOV+PDDD1WvXj09/PDDbseHDBmijh07qkGDBlq/fr1SUlJ0+PBhTZ06VZLkdDoVHh7u9prAwECzrX79+nI6neax82ucTuclx5OWlqZx48ZVxtQAAEA1UG1C06xZs9S/f395e3u7HR8xYoT5c9u2bWW32/X8888rLS1NDofjmo0nJSXF7dwul0uhoaHX7HwAAKBqVYvQtHbtWuXn52v+/PmXrY2Ojta5c+d08OBBtWjRQkFBQSoqKnKrKd8vvw/qUjWXuk9KkhwOxzUNZQAAwLNUi3uaPvjgA0VFRaldu3aXrc3NzZWXl5cCAgIkSTExMVqzZo3Onj1r1mRmZqpFixaqX7++WZOVleXWT2ZmpmJiYipxFgAAoDqr0tB08uRJ5ebmKjc3V5JUUFCg3NxcFRYWmjUul0sLFizQs88+e8Hrs7Oz9eabb2r79u06cOCA5syZo+HDh+uJJ54wA9Hjjz8uu92uxMRE7dq1S/Pnz9e0adPcPlobOnSoMjIyNGXKFO3du1epqanasmWLkpOTr+0CAACAaqNKP57bsmWLunfvbu6XB5mBAwcqPT1dkjRv3jwZhqF+/fpd8HqHw6F58+YpNTVVJSUlCg8P1/Dhw90Cka+vr5YtW6akpCRFRUXJ399fY8eONR83IEl33XWX5s6dqzFjxuiVV15R8+bNtXDhQrVp0+YazRwAAFQ3NsMwjKoexI3A5XLJ19dXxcXF8vHxqdS+t27dqqioKN3/6mw1aNyi0vo9WpivzD8/rZycHHXs2LHS+gUAoLq4kr/f1eKeJgAAgKpGaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWVGloWrNmjR588EGFhITIZrNp4cKFbu1PPfWUbDab29azZ0+3mqNHj6p///7y8fGRn5+fEhMTdfLkSbeaHTt2qEuXLvL29lZoaKgmTZp0wVgWLFigli1bytvbW5GRkVqyZEmlzxcAAFRfVRqaTp06pXbt2mn69OmXrOnZs6cOHz5sbh999JFbe//+/bVr1y5lZmZq0aJFWrNmjQYNGmS2u1wuxcXFKSwsTDk5OZo8ebJSU1P13nvvmTXr169Xv379lJiYqG3btql3797q3bu38vLyKn/SAACgWqpZlSfv1auXevXqVWGNw+FQUFDQRdv27NmjjIwMbd68WZ06dZIkvf3223rggQf0P//zPwoJCdGcOXN05swZzZo1S3a7Xa1bt1Zubq6mTp1qhqtp06apZ8+eGjlypCRpwoQJyszM1DvvvKOZM2dW4owBAEB15fH3NK1atUoBAQFq0aKFXnjhBf34449mW3Z2tvz8/MzAJEmxsbHy8vLSxo0bzZquXbvKbrebNfHx8crPz9exY8fMmtjYWLfzxsfHKzs7+1pODQAAVCNVeqXpcnr27KmHH35Y4eHh2r9/v1555RX16tVL2dnZqlGjhpxOpwICAtxeU7NmTTVo0EBOp1OS5HQ6FR4e7lYTGBhottWvX19Op9M8dn5NeR8XU1JSopKSEnPf5XL9prkCAADP5tGh6bHHHjN/joyMVNu2bXXbbbdp1apV6tGjRxWOTEpLS9O4ceOqdAwAAOD68fiP587XtGlT+fv7a9++fZKkoKAgHTlyxK3m3LlzOnr0qHkfVFBQkIqKitxqyvcvV3Ope6kkKSUlRcXFxeZ26NCh3zY5AADg0apVaPr3v/+tH3/8UcHBwZKkmJgYHT9+XDk5OWbNihUrVFZWpujoaLNmzZo1Onv2rFmTmZmpFi1aqH79+mZNVlaW27kyMzMVExNzybE4HA75+Pi4bQAA4MZVpaHp5MmTys3NVW5uriSpoKBAubm5Kiws1MmTJzVy5Eht2LBBBw8eVFZWlh566CE1a9ZM8fHxkqRWrVqpZ8+eeu6557Rp0yZ9+eWXSk5O1mOPPaaQkBBJ0uOPPy673a7ExETt2rVL8+fP17Rp0zRixAhzHEOHDlVGRoamTJmivXv3KjU1VVu2bFFycvJ1XxMAAOCZqjQ0bdmyRR06dFCHDh0kSSNGjFCHDh00duxY1ahRQzt27NAf/vAH3X777UpMTFRUVJTWrl0rh8Nh9jFnzhy1bNlSPXr00AMPPKB77rnH7RlMvr6+WrZsmQoKChQVFaWXXnpJY8eOdXuW01133aW5c+fqvffeU7t27fSPf/xDCxcuVJs2ba7fYgAAAI9WpTeCd+vWTYZhXLJ96dKll+2jQYMGmjt3boU1bdu21dq1ayuseeSRR/TII49c9nwAAODmVK3uaQIAAKgqhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAgioNTWvWrNGDDz6okJAQ2Ww2LVy40Gw7e/asRo8ercjISN1yyy0KCQnRgAED9N1337n10aRJE9lsNrdt4sSJbjU7duxQly5d5O3trdDQUE2aNOmCsSxYsEAtW7aUt7e3IiMjtWTJkmsyZwAAUD1VaWg6deqU2rVrp+nTp1/Q9tNPP2nr1q167bXXtHXrVn3yySfKz8/XH/7whwtqx48fr8OHD5vbiy++aLa5XC7FxcUpLCxMOTk5mjx5slJTU/Xee++ZNevXr1e/fv2UmJiobdu2qXfv3urdu7fy8vKuzcQBAEC1U7MqT96rVy/16tXrom2+vr7KzMx0O/bOO+/ojjvuUGFhoRo3bmwer1evnoKCgi7az5w5c3TmzBnNmjVLdrtdrVu3Vm5urqZOnapBgwZJkqZNm6aePXtq5MiRkqQJEyYoMzNT77zzjmbOnFkZUwUAANVctbqnqbi4WDabTX5+fm7HJ06cqIYNG6pDhw6aPHmyzp07Z7ZlZ2era9eustvt5rH4+Hjl5+fr2LFjZk1sbKxbn/Hx8crOzr52kwEAANVKlV5puhI///yzRo8erX79+snHx8c8PmTIEHXs2FENGjTQ+vXrlZKSosOHD2vq1KmSJKfTqfDwcLe+AgMDzbb69evL6XSax86vcTqdlxxPSUmJSkpKzH2Xy/Wb5wgAADxXtQhNZ8+e1aOPPirDMDRjxgy3thEjRpg/t23bVna7Xc8//7zS0tLkcDiu2ZjS0tI0bty4a9Y/AADwLB7/8Vx5YPrmm2+UmZnpdpXpYqKjo3Xu3DkdPHhQkhQUFKSioiK3mvL98vugLlVzqfukJCklJUXFxcXmdujQoSudGgAAqEY8OjSVB6avv/5ay5cvV8OGDS/7mtzcXHl5eSkgIECSFBMTozVr1ujs2bNmTWZmplq0aKH69eubNVlZWW79ZGZmKiYm5pLncTgc8vHxcdsAAMCNq0o/njt58qT27dtn7hcUFCg3N1cNGjRQcHCw/vSnP2nr1q1atGiRSktLzXuMGjRoILvdruzsbG3cuFHdu3dXvXr1lJ2dreHDh+uJJ54wA9Hjjz+ucePGKTExUaNHj1ZeXp6mTZumN954wzzv0KFDde+992rKlClKSEjQvHnztGXLFrfHEgAAgJtblYamLVu2qHv37uZ++f1JAwcOVGpqqj7//HNJUvv27d1et3LlSnXr1k0Oh0Pz5s1TamqqSkpKFB4eruHDh7vd5+Tr66tly5YpKSlJUVFR8vf319ixY83HDUjSXXfdpblz52rMmDF65ZVX1Lx5cy1cuFBt2rS5hrMHAADVSZWGpm7duskwjEu2V9QmSR07dtSGDRsue562bdtq7dq1FdY88sgjeuSRRy7bFwAAuDl59D1NAAAAnoLQBAAAYAGhCQAAwAJCEwAAgAVXFZqaNm2qH3/88YLjx48fV9OmTX/zoAAAADzNVYWmgwcPqrS09ILjJSUl+vbbb3/zoAAAADzNFT1yoPy5SZK0dOlS+fr6mvulpaXKyspSkyZNKm1wAAAAnuKKQlPv3r0lSTabTQMHDnRrq1Wrlpo0aaIpU6ZU2uAAAAA8xRWFprKyMklSeHi4Nm/eLH9//2syKAAAAE9zVU8ELygoqOxxAAAAeLSr/jcqWVlZysrK0pEjR8wrUOVmzZr1mwcGAADgSa4qNI0bN07jx49Xp06dFBwcLJvNVtnjAgAA8ChXFZpmzpyp9PR0Pfnkk5U9HgAAAI90Vc9pOnPmjO66667KHgsAAIDHuqrQ9Oyzz2ru3LmVPRYAAACPdVUfz/3888967733tHz5crVt21a1atVya586dWqlDA4AAMBTXFVo2rFjh9q3by9JysvLc2vjpnAAAHAjuqrQtHLlysoeBwAAgEe7qnuaAAAAbjZXdaWpe/fuFX4Mt2LFiqseEAAAgCe6qtBUfj9TubNnzyo3N1d5eXkX/CNfAACAG8FVhaY33njjosdTU1N18uTJ3zQgAAAAT1Sp9zQ98cQT/N85AABwQ6rU0JSdnS1vb+/K7BIAAMAjXNXHcw8//LDbvmEYOnz4sLZs2aLXXnutUgYGAADgSa4qNPn6+rrte3l5qUWLFho/frzi4uIqZWAAAACe5KpC0+zZsyt7HAAAAB7tqkJTuZycHO3Zs0eS1Lp1a3Xo0KFSBgUAAOBprio0HTlyRI899phWrVolPz8/SdLx48fVvXt3zZs3T40aNarMMQIAAFS5q/r23IsvvqgTJ05o165dOnr0qI4ePaq8vDy5XC4NGTKksscIAABQ5a7qSlNGRoaWL1+uVq1amcciIiI0ffp0bgQHAAA3pKu60lRWVqZatWpdcLxWrVoqKyv7zYMCAADwNFcVmu677z4NHTpU3333nXns22+/1fDhw9WjRw/L/axZs0YPPvigQkJCZLPZtHDhQrd2wzA0duxYBQcHq3bt2oqNjdXXX3/tVnP06FH1799fPj4+8vPzU2Ji4gX/ymXHjh3q0qWLvL29FRoaqkmTJl0wlgULFqhly5by9vZWZGSklixZYnkeAADgxndVoemdd96Ry+VSkyZNdNttt+m2225TeHi4XC6X3n77bcv9nDp1Su3atdP06dMv2j5p0iS99dZbmjlzpjZu3KhbbrlF8fHx+vnnn82a/v37a9euXcrMzNSiRYu0Zs0aDRo0yGx3uVyKi4tTWFiYcnJyNHnyZKWmpuq9994za9avX69+/fopMTFR27ZtU+/evdW7d2/l5eVdxeoAAIAbkc0wDONqXmgYhpYvX669e/dKklq1aqXY2NirH4jNpk8//VS9e/c2+w8JCdFLL72k//zP/5QkFRcXKzAwUOnp6Xrssce0Z88eRUREaPPmzerUqZOkX+63euCBB/Tvf/9bISEhmjFjhl599VU5nU7Z7XZJ0ssvv6yFCxeaY+/bt69OnTqlRYsWmeO588471b59e82cOdPS+F0ul3x9fVVcXCwfH5+rXoeL2bp1q6KionT/q7PVoHGLSuv3aGG+Mv/8tHJyctSxY8dK6xcAgOriSv5+X9GVphUrVigiIkIul0s2m03333+/XnzxRb344ovq3LmzWrdurbVr1/6mwZcrKCiQ0+l0C2K+vr6Kjo5Wdna2pF/+152fn58ZmCQpNjZWXl5e2rhxo1nTtWtXMzBJUnx8vPLz83Xs2DGz5teBLz4+3jzPxZSUlMjlcrltAADgxnVFoenNN9/Uc889d9Ek5uvrq+eff15Tp06tlIE5nU5JUmBgoNvxwMBAs83pdCogIMCtvWbNmmrQoIFbzcX6OP8cl6opb7+YtLQ0+fr6mltoaOiVThEAAFQjVxSatm/frp49e16yPS4uTjk5Ob95UNVBSkqKiouLze3QoUNVPSQAAHANXVFoKioquuijBsrVrFlT33///W8elCQFBQWZ5/z1GMrbgoKCdOTIEbf2c+fO6ejRo241F+vj/HNcqqa8/WIcDod8fHzcNgAAcOO6otD0u9/9rsJvlO3YsUPBwcG/eVCSFB4erqCgIGVlZZnHXC6XNm7cqJiYGElSTEyMjh8/7nZ1a8WKFSorK1N0dLRZs2bNGp09e9asyczMVIsWLVS/fn2z5vzzlNeUnwcAAOCKQtMDDzyg1157ze0r/+VOnz6t119/Xb///e8t93fy5Enl5uYqNzdX0i83f+fm5qqwsFA2m03Dhg3Tf/3Xf+nzzz/Xzp07NWDAAIWEhJjfsGvVqpV69uyp5557Tps2bdKXX36p5ORkPfbYYwoJCZEkPf7447Lb7UpMTNSuXbs0f/58TZs2TSNGjDDHMXToUGVkZGjKlCnau3evUlNTtWXLFiUnJ1/J8gAAgBvYFf0blTFjxuiTTz7R7bffruTkZLVo8cvX3/fu3avp06ertLRUr776quX+tmzZou7du5v75UFm4MCBSk9P16hRo3Tq1CkNGjRIx48f1z333KOMjAx5e3ubr5kzZ46Sk5PVo0cPeXl5qU+fPnrrrbfMdl9fXy1btkxJSUmKioqSv7+/xo4d6/Ysp7vuuktz587VmDFj9Morr6h58+ZauHCh2rRpcyXLAwAAbmBX/Jymb775Ri+88IKWLl2q8pfabDbFx8dr+vTpCg8PvyYD9XQ8pwkAgOrnSv5+X/E/7A0LC9OSJUt07Ngx7du3T4ZhqHnz5ub9QQAAADeiKw5N5erXr6/OnTtX5lgAAAA81lX97zkAAICbDaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALPD40NSkSRPZbLYLtqSkJElSt27dLmgbPHiwWx+FhYVKSEhQnTp1FBAQoJEjR+rcuXNuNatWrVLHjh3lcDjUrFkzpaenX68pAgCAaqBmVQ/gcjZv3qzS0lJzPy8vT/fff78eeeQR89hzzz2n8ePHm/t16tQxfy4tLVVCQoKCgoK0fv16HT58WAMGDFCtWrX0l7/8RZJUUFCghIQEDR48WHPmzFFWVpaeffZZBQcHKz4+/jrMEgAAeDqPD02NGjVy2584caJuu+023XvvveaxOnXqKCgo6KKvX7ZsmXbv3q3ly5crMDBQ7du314QJEzR69GilpqbKbrdr5syZCg8P15QpUyRJrVq10rp16/TGG28QmgAAgKRq8PHc+c6cOaO///3veuaZZ2Sz2czjc+bMkb+/v9q0aaOUlBT99NNPZlt2drYiIyMVGBhoHouPj5fL5dKuXbvMmtjYWLdzxcfHKzs7+5JjKSkpkcvlctsAAMCNy+OvNJ1v4cKFOn78uJ566inz2OOPP66wsDCFhIRox44dGj16tPLz8/XJJ59IkpxOp1tgkmTuO53OCmtcLpdOnz6t2rVrXzCWtLQ0jRs3rjKnBwAAPFi1Ck0ffPCBevXqpZCQEPPYoEGDzJ8jIyMVHBysHj16aP/+/brtttuu2VhSUlI0YsQIc9/lcik0NPSanQ8AAFStahOavvnmGy1fvty8gnQp0dHRkqR9+/bptttuU1BQkDZt2uRWU1RUJEnmfVBBQUHmsfNrfHx8LnqVSZIcDoccDsdVzQUAAFQ/1eaeptmzZysgIEAJCQkV1uXm5kqSgoODJUkxMTHauXOnjhw5YtZkZmbKx8dHERERZk1WVpZbP5mZmYqJianEGQAAgOqsWoSmsrIyzZ49WwMHDlTNmv//xbH9+/drwoQJysnJ0cGDB/X5559rwIAB6tq1q9q2bStJiouLU0REhJ588klt375dS5cu1ZgxY5SUlGReKRo8eLAOHDigUaNGae/evXr33Xf18ccfa/jw4VUyXwAA4HmqRWhavny5CgsL9cwzz7gdt9vtWr58ueLi4tSyZUu99NJL6tOnj7744guzpkaNGlq0aJFq1KihmJgYPfHEExowYIDbc53Cw8O1ePFiZWZmql27dpoyZYref/99HjcAAABM1eKepri4OBmGccHx0NBQrV69+rKvDwsL05IlSyqs6datm7Zt23bVYwQAADe2anGlCQAAoKoRmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALPDo0paamymazuW0tW7Y023/++WclJSWpYcOGqlu3rvr06aOioiK3PgoLC5WQkKA6deooICBAI0eO1Llz59xqVq1apY4dO8rhcKhZs2ZKT0+/HtMDAADViEeHJklq3bq1Dh8+bG7r1q0z24YPH64vvvhCCxYs0OrVq/Xdd9/p4YcfNttLS0uVkJCgM2fOaP369frwww+Vnp6usWPHmjUFBQVKSEhQ9+7dlZubq2HDhunZZ5/V0qVLr+s8AQCAZ6tZ1QO4nJo1ayooKOiC48XFxfrggw80d+5c3XfffZKk2bNnq1WrVtqwYYPuvPNOLVu2TLt379by5csVGBio9u3ba8KECRo9erRSU1Nlt9s1c+ZMhYeHa8qUKZKkVq1aad26dXrjjTcUHx9/XecKAAA8l8dfafr6668VEhKipk2bqn///iosLJQk5eTk6OzZs4qNjTVrW7ZsqcaNGys7O1uSlJ2drcjISAUGBpo18fHxcrlc2rVrl1lzfh/lNeV9XEpJSYlcLpfbBgAAblweHZqio6OVnp6ujIwMzZgxQwUFBerSpYtOnDghp9Mpu90uPz8/t9cEBgbK6XRKkpxOp1tgKm8vb6uoxuVy6fTp05ccW1pamnx9fc0tNDT0t04XAAB4MI/+eK5Xr17mz23btlV0dLTCwsL08ccfq3bt2lU4MiklJUUjRoww910uF8EJAIAbmEdfafo1Pz8/3X777dq3b5+CgoJ05swZHT9+3K2mqKjIvAcqKCjogm/Tle9frsbHx6fCYOZwOOTj4+O2AQCAG1e1Ck0nT57U/v37FRwcrKioKNWqVUtZWVlme35+vgoLCxUTEyNJiomJ0c6dO3XkyBGzJjMzUz4+PoqIiDBrzu+jvKa8DwAAAMnDQ9N//ud/avXq1Tp48KDWr1+vP/7xj6pRo4b69esnX19fJSYmasSIEVq5cqVycnL09NNPKyYmRnfeeackKS4uThEREXryySe1fft2LV26VGPGjFFSUpIcDockafDgwTpw4IBGjRqlvXv36t1339XHH3+s4cOHV+XUAQCAh/Hoe5r+/e9/q1+/fvrxxx/VqFEj3XPPPdqwYYMaNWokSXrjjTfk5eWlPn36qKSkRPHx8Xr33XfN19eoUUOLFi3SCy+8oJiYGN1yyy0aOHCgxo8fb9aEh4dr8eLFGj58uKZNm6Zbb71V77//Po8bAAAAbjw6NM2bN6/Cdm9vb02fPl3Tp0+/ZE1YWJiWLFlSYT/dunXTtm3brmqMAADg5uDRH88BAAB4CkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsMCjQ1NaWpo6d+6sevXqKSAgQL1791Z+fr5bTbdu3WSz2dy2wYMHu9UUFhYqISFBderUUUBAgEaOHKlz58651axatUodO3aUw+FQs2bNlJ6efq2nBwAAqhGPDk2rV69WUlKSNmzYoMzMTJ09e1ZxcXE6deqUW91zzz2nw4cPm9ukSZPMttLSUiUkJOjMmTNav369PvzwQ6Wnp2vs2LFmTUFBgRISEtS9e3fl5uZq2LBhevbZZ7V06dLrNlcAAODZalb1ACqSkZHhtp+enq6AgADl5OSoa9eu5vE6deooKCjoon0sW7ZMu3fv1vLlyxUYGKj27dtrwoQJGj16tFJTU2W32zVz5kyFh4drypQpkqRWrVpp3bp1euONNxQfH3/tJggAAKoNj77S9GvFxcWSpAYNGrgdnzNnjvz9/dWmTRulpKTop59+Mtuys7MVGRmpwMBA81h8fLxcLpd27dpl1sTGxrr1GR8fr+zs7Gs1FQAAUM149JWm85WVlWnYsGG6++671aZNG/P4448/rrCwMIWEhGjHjh0aPXq08vPz9cknn0iSnE6nW2CSZO47nc4Ka1wul06fPq3atWtfMJ6SkhKVlJSY+y6Xq3ImCgAAPFK1CU1JSUnKy8vTunXr3I4PGjTI/DkyMlLBwcHq0aOH9u/fr9tuu+2ajSctLU3jxo27Zv0DAADPUi0+nktOTtaiRYu0cuVK3XrrrRXWRkdHS5L27dsnSQoKClJRUZFbTfl++X1Ql6rx8fG56FUmSUpJSVFxcbG5HTp06MonBgAAqg2PDk2GYSg5OVmffvqpVqxYofDw8Mu+Jjc3V5IUHBwsSYqJidHOnTt15MgRsyYzM1M+Pj6KiIgwa7Kystz6yczMVExMzCXP43A45OPj47YBAIAbl0eHpqSkJP3973/X3LlzVa9ePTmdTjmdTp0+fVqStH//fk2YMEE5OTk6ePCgPv/8cw0YMEBdu3ZV27ZtJUlxcXGKiIjQk08+qe3bt2vp0qUaM2aMkpKS5HA4JEmDBw/WgQMHNGrUKO3du1fvvvuuPv74Yw0fPrzK5g4AADyLR4emGTNmqLi4WN26dVNwcLC5zZ8/X5Jkt9u1fPlyxcXFqWXLlnrppZfUp08fffHFF2YfNWrU0KJFi1SjRg3FxMToiSee0IABAzR+/HizJjw8XIsXL1ZmZqbatWunKVOm6P333+dxAwAAwOTRN4IbhlFhe2hoqFavXn3ZfsLCwrRkyZIKa7p166Zt27Zd0fgAAMDNw6OvNAEAAHgKQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFhCaAAAALCA0AQAAWEBoAgAAsIDQBAAAYAGhCQAAwAJCEwAAgAWEJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAAAALCE0AAAAWEJoAAAAsIDQBAABYQGgCAACwgNAEAABgAaHpV6ZPn64mTZrI29tb0dHR2rRpU1UPCQAAeABC03nmz5+vESNG6PXXX9fWrVvVrl07xcfH68iRI1U9NAAAUMUITeeZOnWqnnvuOT399NOKiIjQzJkzVadOHc2aNauqhwYAAKpYzaoegKc4c+aMcnJylJKSYh7z8vJSbGyssrOzq3Bk196ePXuuSb/+/v5q3LjxNekbAIDrjdD0f3744QeVlpYqMDDQ7XhgYKD27t17QX1JSYlKSkrM/eLiYkmSy+Wq9LGdPHlSknT0m3ydKzldaf3+sH+nJOmJJ56otD7P53B46//9v79dsKa/lZeXl8rKyiq1z2vdd3Uc87XsmzFfn74Z8/XpuzqO+Vr2fS3HHBQUpKCgoErts/zvtmEYl60lNF2ltLQ0jRs37oLjoaGh1+ycOX+feM36vhZKSn7Wo48+WtXDAADgsk6cOCFfX98KawhN/8ff3181atRQUVGR2/GioqKLptqUlBSNGDHC3C8rK9PRo0fVsGFD2Wy2ShuXy+VSaGioDh06JB8fn0rr90bB+lSM9akY61Mx1qdirE/Fqsv6GIahEydOKCQk5LK1hKb/Y7fbFRUVpaysLPXu3VvSL0EoKytLycnJF9Q7HA45HA63Y35+ftdsfD4+Ph79S1fVWJ+KsT4VY30qxvpUjPWpWHVYn8tdYSpHaDrPiBEjNHDgQHXq1El33HGH3nzzTZ06dUpPP/10VQ8NAABUMULTefr27avvv/9eY8eOldPpVPv27ZWRkVHpNzIDAIDqh9D0K8nJyRf9OK6qOBwOvf766xd8FIhfsD4VY30qxvpUjPWpGOtTsRtxfWyGle/YAQAA3OR4IjgAAIAFhCYAAAALCE0AAAAWEJo83PTp09WkSRN5e3srOjpamzZtquohVbo1a9bowQcfVEhIiGw2mxYuXOjWbhiGxo4dq+DgYNWuXVuxsbH6+uuv3WqOHj2q/v37y8fHR35+fkpMTDT//Uy5HTt2qEuXLvL29lZoaKgmTZp0radWKdLS0tS5c2fVq1dPAQEB6t27t/Lz891qfv75ZyUlJalhw4aqW7eu+vTpc8GDWgsLC5WQkKA6deooICBAI0eO1Llz59xqVq1apY4dO8rhcKhZs2ZKT0+/1tP7zWbMmKG2bduaz4KJiYnRv/71L7P9Zl6bX5s4caJsNpuGDRtmHrvZ1yc1NVU2m81ta9mypdl+s6/Pt99+qyeeeEINGzZU7dq1FRkZqS1btpjtN937swGPNW/ePMNutxuzZs0ydu3aZTz33HOGn5+fUVRUVNVDq1RLliwxXn31VeOTTz4xJBmffvqpW/vEiRMNX19fY+HChcb27duNP/zhD0Z4eLhx+vRps6Znz55Gu3btjA0bNhhr1641mjVrZvTr189sLy4uNgIDA43+/fsbeXl5xkcffWTUrl3b+N///d/rNc2rFh8fb8yePdvIy8szcnNzjQceeMBo3LixcfLkSbNm8ODBRmhoqJGVlWVs2bLFuPPOO4277rrLbD937pzRpk0bIzY21ti2bZuxZMkSw9/f30hJSTFrDhw4YNSpU8cYMWKEsXv3buPtt982atSoYWRkZFzX+V6pzz//3Fi8eLHx1VdfGfn5+cYrr7xi1KpVy8jLyzMM4+Zem/Nt2rTJaNKkidG2bVtj6NCh5vGbfX1ef/11o3Xr1sbhw4fN7fvvvzfbb+b1OXr0qBEWFmY89dRTxsaNG40DBw4YS5cuNfbt22fW3Gzvz4QmD3bHHXcYSUlJ5n5paakREhJipKWlVeGorq1fh6aysjIjKCjImDx5snns+PHjhsPhMD766CPDMAxj9+7dhiRj8+bNZs2//vUvw2azGd9++61hGIbx7rvvGvXr1zdKSkrMmtGjRxstWrS4xjOqfEeOHDEkGatXrzYM45f1qFWrlrFgwQKzZs+ePYYkIzs72zCMX4Kpl5eX4XQ6zZoZM2YYPj4+5pqMGjXKaN26tdu5+vbta8THx1/rKVW6+vXrG++//z5r839OnDhhNG/e3MjMzDTuvfdeMzSxPr+Epnbt2l207WZfn9GjRxv33HPPJdtvxvdnPp7zUGfOnFFOTo5iY2PNY15eXoqNjVV2dnYVjuz6KigokNPpdFsHX19fRUdHm+uQnZ0tPz8/derUyayJjY2Vl5eXNm7caNZ07dpVdrvdrImPj1d+fr6OHTt2nWZTOYqLiyVJDRo0kCTl5OTo7NmzbmvUsmVLNW7c2G2NIiMj3R7UGh8fL5fLpV27dpk15/dRXlOdft9KS0s1b948nTp1SjExMazN/0lKSlJCQsIFc2B9fvH1118rJCRETZs2Vf/+/VVYWCiJ9fn888/VqVMnPfLIIwoICFCHDh3017/+1Wy/Gd+fCU0e6ocfflBpaekFTyMPDAyU0+msolFdf+VzrWgdnE6nAgIC3Npr1qypBg0auNVcrI/zz1EdlJWVadiwYbr77rvVpk0bSb+M3263X/C/D3+9Rpeb/6VqXC6XTp8+fS2mU2l27typunXryuFwaPDgwfr0008VERHB2kiaN2+etm7dqrS0tAvaWB8pOjpa6enpysjI0IwZM1RQUKAuXbroxIkTN/36HDhwQDNmzFDz5s21dOlSvfDCCxoyZIg+/PBDSTfn+zNPBAeqkaSkJOXl5WndunVVPRSP0qJFC+Xm5qq4uFj/+Mc/NHDgQK1evbqqh1XlDh06pKFDhyozM1Pe3t5VPRyP1KtXL/Pntm3bKjo6WmFhYfr4449Vu3btKhxZ1SsrK1OnTp30l7/8RZLUoUMH5eXlaebMmRo4cGAVj65qcKXJQ/n7+6tGjRoXfEujqKhIQUFBVTSq6698rhWtQ1BQkI4cOeLWfu7cOR09etSt5mJ9nH8OT5ecnKxFixZp5cqVuvXWW83jQUFBOnPmjI4fP+5W/+s1utz8L1Xj4+Pj8X887Ha7mjVrpqioKKWlpaldu3aaNm3aTb82OTk5OnLkiDp27KiaNWuqZs2aWr16td566y3VrFlTgYGBN/X6XIyfn59uv/127du376b//QkODlZERITbsVatWpkfX96M78+EJg9lt9sVFRWlrKws81hZWZmysrIUExNThSO7vsLDwxUUFOS2Di6XSxs3bjTXISYmRsePH1dOTo5Zs2LFCpWVlSk6OtqsWbNmjc6ePWvWZGZmqkWLFqpfv/51ms3VMQxDycnJ+vTTT7VixQqFh4e7tUdFRalWrVpua5Sfn6/CwkK3Ndq5c6fbm1dmZqZ8fHzMN8WYmBi3PsprquPvW1lZmUpKSm76tenRo4d27typ3Nxcc+vUqZP69+9v/nwzr8/FnDx5Uvv371dwcPBN//tz9913X/B4k6+++kphYWGSbtL356q+Ex2XNm/ePMPhcBjp6enG7t27jUGDBhl+fn5u39K4EZw4ccLYtm2bsW3bNkOSMXXqVGPbtm3GN998YxjGL19p9fPzMz777DNjx44dxkMPPXTRr7R26NDB2Lhxo7Fu3TqjefPmbl9pPX78uBEYGGg8+eSTRl5enjFv3jyjTp06HvmV1l974YUXDF9fX2PVqlVuX4v+6aefzJrBgwcbjRs3NlasWGFs2bLFiImJMWJiYsz28q9Fx8XFGbm5uUZGRobRqFGji34teuTIkcaePXuM6dOnV4uvRb/88svG6tWrjYKCAmPHjh3Gyy+/bNhsNmPZsmWGYdzca3Mx5397zjBYn5deeslYtWqVUVBQYHz55ZdGbGys4e/vbxw5csQwjJt7fTZt2mTUrFnT+POf/2x8/fXXxpw5c4w6deoYf//7382am+39mdDk4d5++22jcePGht1uN+644w5jw4YNVT2kSrdy5UpD0gXbwIEDDcP45Wutr732mhEYGGg4HA6jR48eRn5+vlsfP/74o9GvXz+jbt26ho+Pj/H0008bJ06ccKvZvn27cc899xgOh8P43e9+Z0ycOPF6TfE3udjaSDJmz55t1pw+fdr4j//4D6N+/fpGnTp1jD/+8Y/G4cOH3fo5ePCg0atXL6N27dqGv7+/8dJLLxlnz551q1m5cqXRvn17w263G02bNnU7h6d65plnjLCwMMNutxuNGjUyevToYQYmw7i51+Zifh2abvb16du3rxEcHGzY7Xbjd7/7ndG3b1+35xDd7OvzxRdfGG3atDEcDofRsmVL47333nNrv9nen22GYRhVc40LAACg+uCeJgAAAAsITQAAABYQmgAAACwgNAEAAFhAaAIAALCA0AQAAGABoQkAAMACQhMAAIAFhCYAN4WDBw/KZrMpNze3qocCoJriieAAbgqlpaX6/vvv5e/vr5o1a1b1cABUQ4QmANXCmTNnZLfbq3oYAG5ifDwHwCN169ZNycnJGjZsmPz9/RUfH6+8vDz16tVLdevWVWBgoJ588kn98MMP5mvKyso0adIkNWvWTA6HQ40bN9af//xnSRd+PLdq1SrZbDYtXrxYbdu2lbe3t+68807l5eW5jWPdunXq0qWLateurdDQUA0ZMkSnTp2yNId3331XzZs3l7e3twIDA/WnP/3JbX5DhgzRqFGj1KBBAwUFBSk1NdXt9YWFhXrooYdUt25d+fj46NFHH1VRUdFVrCaAykBoAuCxPvzwQ9ntdn355ZeaOHGi7rvvPnXo0EFbtmxRRkaGioqK9Oijj5r1KSkpmjhxol577TXt3r1bc+fOVWBgYIXnGDlypKZMmaLNmzerUaNGevDBB3X27FlJ0v79+9WzZ0/16dNHO3bs0Pz587Vu3TolJydfduxbtmzRkCFDNH78eOXn5ysjI0Ndu3a9YH633HKLNm7cqEmTJmn8+PHKzMyU9EsAfOihh3T06FGtXr1amZmZOnDggPr27XulywigshgA4IHuvfdeo0OHDub+hAkTjLi4OLeaQ4cOGZKM/Px8w+VyGQ6Hw/jrX/960f4KCgoMSca2bdsMwzCMlStXGpKMefPmmTU//vijUbt2bWP+/PmGYRhGYmKiMWjQILd+1q5da3h5eRmnT5+ucPz//Oc/DR8fH8Plcl1yfvfcc4/bsc6dOxujR482DMMwli1bZtSoUcMoLCw023ft2mVIMjZt2lThuQFcG9wNCcBjRUVFmT9v375dK1euVN26dS+o279/v44fP66SkhL16NHjis4RExNj/tygQQO1aNFCe/bsMc+5Y8cOzZkzx6wxDENlZWUqKChQq1atLtnv/fffr7CwMDVt2lQ9e/ZUz5499cc//lF16tQxa9q2bev2muDgYB05ckSStGfPHoWGhio0NNRsj4iIkJ+fn/bs2aPOnTtf0TwB/HaEJgAe65ZbbjF/PnnypB588EH993//9wV1wcHBOnDgQKWf/+TJk3r++ec1ZMiQC9oaN25c4Wvr1aunrVu3atWqVVq2bJnGjh2r1NRUbd68WX5+fpKkWrVqub3GZrOprKys0sYPoHIRmgBUCx07dtQ///lPNWnS5KKPDGjevLlq166trKwsPfvss5b73bBhgxmAjh07pq+++sq8gtSxY0ft3r1bzZo1u6ox16xZU7GxsYqNjdXrr78uPz8/rVixQg8//PBlX9uqVSsdOnRIhw4dMq827d69W8ePH1dERMRVjQfAb8ON4ACqhaSkJB09elT9+vXT5s2btX//fi1dulRPP/20SktL5e3trdGjR2vUqFH629/+pv3792vDhg364IMPKux3/PjxysrKUl5enp566in5+/urd+/ekqTRo0dr/fr1Sk5OVm5urr7++mt99tlnlm4EX7Rokd566y3l5ubqm2++0d/+9jeVlZWpRYsWluYbGxuryMhI9e/fX1u3btWmTZs0YMAA3XvvverUqZOlPgBULkITgGohJCREX375pUpLSxUXF6fIyEgNGzZMfn5+8vL65a3stdde00svvaSxY8eqVatW6tu3r3mP0KVMnDhRQ4cOVVRUlJxOp7744gvzeVBt27bV6tWr9dVXX6lLly7q0KGDxo4dq5CQkMuO18/PT5988onuu+8+tWrVSjNnztRHH32k1q1bW5qvzWbTZ599pvr166tr166KjY1V06ZNNX/+fEuvB1D5eLglgJvSqlWr1L17dx07dsy8xwgAKsKVJgAAAAsITQBwFdauXau6detecgNw4+HjOQC4CqdPn9a33357yfar/cYdAM9FaAIAALCAj+cAAAAsIDQBAABYQGgCAACwgNAEAABgAaEJAADAAkITAACABYQmAAAACwhNAAAAFvx/glxBO/1O+nsAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# sequence length\n", + "sns.histplot(seq_df.recipe_sno.apply(len), bins=20)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> 데이터가 좌측으로 매우 치우침" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "count 20588.000000\n", + "mean 13.447008\n", + "std 58.350925\n", + "min 5.000000\n", + "25% 6.000000\n", + "50% 8.000000\n", + "75% 13.000000\n", + "max 6273.000000\n", + "Name: recipe_sno, dtype: float64" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "seq_df.recipe_sno.apply(len).describe()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> 75% 의 유저가 13개 이하의 인터렉션을 가지며, 극도로 많은 인터렉션을 가지는 유저도 존재함" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjsAAAGxCAYAAACEFXd4AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/H5lhTAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAxcUlEQVR4nO3de1yVZb7//zfIQRQXhMgCS1DLBMqzpaupmVISzd2u0dlTjTo0OSc3mGa7zCnPu9Ht7Om4KXdt02ZPZrWno5WpqHQQTSlKFMnKwlGByHB5QEC4vn/0Y/1mpZYsFqzF5ev5eNyPh+u+r2vdn0+rR76713WvO8QYYwQAAGCp0EAXAAAA0JoIOwAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAq4UFuoBg0NjYqAMHDqhLly4KCQkJdDkAAOAsGGN05MgRde/eXaGhZ75+Q9iRdODAAfXo0SPQZQAAAB/s27dPF1xwwRmPE3YkdenSRdK3/7AcDkeAqwEAAGfD7XarR48enr/Hz4SwI3m+unI4HIQdAADamR9agsICZQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsRtgBAABWI+wAAACrhQW6ANuVlZWpqqrKp7nx8fFKTk72c0UAAJxbCDutqKysTKmpaaqpOe7T/KioTtq9u4TAAwBACwQ07MybN0/z58/32te3b1/t3r1bknTixAndeeedWrVqlWpra5WZmanHHntMTqfTM76srExTpkzRxo0bFR0draysLC1atEhhYYHPcVVVVaqpOa5ht82VI6lns+a6D36hrU/NV1VVFWEHAIAWCHgiuOSSS7R+/XrP638MKXfccYdef/11vfDCC4qJiVFOTo7GjRun9957T5LU0NCgsWPHKjExUZs3b9bBgwf1y1/+UuHh4frjH//Y5r2ciSOpp+KS+wa6DAAAzkkBDzthYWFKTEw8Zf/hw4e1bNkyrVy5UiNGjJAkLV++XGlpadqyZYuGDx+utWvXateuXVq/fr2cTqcGDhyohQsXaubMmZo3b54iIiLauh0AABBkAn431p49e9S9e3f17t1bEyZMUFlZmSSpsLBQ9fX1ysjI8IxNTU1VcnKyCgoKJEkFBQXq16+f19damZmZcrvd2rlz5xnPWVtbK7fb7bUBAAA7BTTsDBs2TCtWrNCaNWv0+OOPa+/evbrqqqt05MgRlZeXKyIiQrGxsV5znE6nysvLJUnl5eVeQafpeNOxM1m0aJFiYmI8W48ePfzbGAAACBoB/RprzJgxnj/3799fw4YNU0pKip5//nlFRUW12nlnzZqlGTNmeF673W4CDwAAlgr411j/KDY2VhdffLE+/fRTJSYmqq6uTtXV1V5jKioqPGt8EhMTVVFRccrxpmNnEhkZKYfD4bUBAAA7BVXYOXr0qD777DMlJSVpyJAhCg8PV15enud4aWmpysrK5HK5JEkul0s7duxQZWWlZ8y6devkcDiUnp7e5vUDAIDgE9Cvsf7t3/5N119/vVJSUnTgwAHNnTtXHTp00C233KKYmBhNnjxZM2bMUFxcnBwOh6ZOnSqXy6Xhw4dLkkaNGqX09HRNmjRJS5YsUXl5ue677z5lZ2crMjIykK0BAIAgEdCw8/e//1233HKLvv76a3Xr1k1XXnmltmzZom7dukmSHnzwQYWGhmr8+PFePyrYpEOHDlq9erWmTJkil8ulzp07KysrSwsWLAhUSwAAIMgENOysWrXqe4937NhRubm5ys3NPeOYlJQUvfHGG/4uDQAAWCKo1uwAAAD4G2EHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALAaYQcAAFiNsAMAAKxG2AEAAFYj7AAAAKsRdgAAgNUIOwAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALAaYQcAAFiNsAMAAKxG2AEAAFYj7AAAAKsRdgAAgNUIOwAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALAaYQcAAFgtaMLO4sWLFRISounTp3v2nThxQtnZ2eratauio6M1fvx4VVRUeM0rKyvT2LFj1alTJyUkJOiuu+7SyZMn27h6AAAQrIIi7Gzbtk3//d//rf79+3vtv+OOO/Taa6/phRdeUH5+vg4cOKBx48Z5jjc0NGjs2LGqq6vT5s2b9fTTT2vFihWaM2dOW7cAAACCVMDDztGjRzVhwgQ9+eSTOu+88zz7Dx8+rGXLlumBBx7QiBEjNGTIEC1fvlybN2/Wli1bJElr167Vrl279Ne//lUDBw7UmDFjtHDhQuXm5qquri5QLQEAgCAS8LCTnZ2tsWPHKiMjw2t/YWGh6uvrvfanpqYqOTlZBQUFkqSCggL169dPTqfTMyYzM1Nut1s7d+484zlra2vldru9NgAAYKewQJ581apV+uCDD7Rt27ZTjpWXlysiIkKxsbFe+51Op8rLyz1j/jHoNB1vOnYmixYt0vz581tYPQAAaA8CdmVn3759mjZtmp555hl17NixTc89a9YsHT582LPt27evTc8PAADaTsDCTmFhoSorKzV48GCFhYUpLCxM+fn5euSRRxQWFian06m6ujpVV1d7zauoqFBiYqIkKTEx8ZS7s5peN405ncjISDkcDq8NAADYKWBhZ+TIkdqxY4eKioo829ChQzVhwgTPn8PDw5WXl+eZU1paqrKyMrlcLkmSy+XSjh07VFlZ6Rmzbt06ORwOpaent3lPAAAg+ARszU6XLl106aWXeu3r3Lmzunbt6tk/efJkzZgxQ3FxcXI4HJo6dapcLpeGDx8uSRo1apTS09M1adIkLVmyROXl5brvvvuUnZ2tyMjINu8JAAAEn4AuUP4hDz74oEJDQzV+/HjV1tYqMzNTjz32mOd4hw4dtHr1ak2ZMkUul0udO3dWVlaWFixYEMCqAQBAMAmqsLNp0yav1x07dlRubq5yc3PPOCclJUVvvPFGK1cGAADaq4D/zg4AAEBrIuwAAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALAaYQcAAFiNsAMAAKxG2AEAAFYj7AAAAKsRdgAAgNUIOwAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALBaWKALwPcrKSlp9pz4+HglJye3QjUAALQ/hJ0gVXP4a0khmjhxYrPnRkV10u7dJQQeAABE2Ala9cePSDIa+IuZ6tYr9aznuQ9+oa1PzVdVVRVhBwAAEXaCXnRCsuKS+wa6DAAA2i0WKAMAAKsRdgAAgNUIOwAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALAaYQcAAFiNsAMAAKxG2AEAAFYLaNh5/PHH1b9/fzkcDjkcDrlcLr355pue4ydOnFB2dra6du2q6OhojR8/XhUVFV7vUVZWprFjx6pTp05KSEjQXXfdpZMnT7Z1KwAAIEgFNOxccMEFWrx4sQoLC7V9+3aNGDFCN9xwg3bu3ClJuuOOO/Taa6/phRdeUH5+vg4cOKBx48Z55jc0NGjs2LGqq6vT5s2b9fTTT2vFihWaM2dOoFoCAABBJiyQJ7/++uu9Xt9///16/PHHtWXLFl1wwQVatmyZVq5cqREjRkiSli9frrS0NG3ZskXDhw/X2rVrtWvXLq1fv15Op1MDBw7UwoULNXPmTM2bN08RERGBaAsAAASRoFmz09DQoFWrVunYsWNyuVwqLCxUfX29MjIyPGNSU1OVnJysgoICSVJBQYH69esnp9PpGZOZmSm32+25OgQAAM5tAb2yI0k7duyQy+XSiRMnFB0drZdeeknp6ekqKipSRESEYmNjvcY7nU6Vl5dLksrLy72CTtPxpmNnUltbq9raWs9rt9vtp24AAECwCfiVnb59+6qoqEhbt27VlClTlJWVpV27drXqORctWqSYmBjP1qNHj1Y9HwAACJyAh52IiAhddNFFGjJkiBYtWqQBAwbo4YcfVmJiourq6lRdXe01vqKiQomJiZKkxMTEU+7OanrdNOZ0Zs2apcOHD3u2ffv2+bcpAAAQNAIedr6rsbFRtbW1GjJkiMLDw5WXl+c5VlpaqrKyMrlcLkmSy+XSjh07VFlZ6Rmzbt06ORwOpaenn/EckZGRntvdmzYAAGCngK7ZmTVrlsaMGaPk5GQdOXJEK1eu1KZNm/TWW28pJiZGkydP1owZMxQXFyeHw6GpU6fK5XJp+PDhkqRRo0YpPT1dkyZN0pIlS1ReXq777rtP2dnZioyMDGRrAAAgSPgUdnr37q1t27apa9euXvurq6s1ePBgff7552f1PpWVlfrlL3+pgwcPKiYmRv3799dbb72la6+9VpL04IMPKjQ0VOPHj1dtba0yMzP12GOPeeZ36NBBq1ev1pQpU+RyudS5c2dlZWVpwYIFvrQFAAAs5FPY+eKLL9TQ0HDK/traWu3fv/+s32fZsmXfe7xjx47Kzc1Vbm7uGcekpKTojTfeOOtzAgCAc0uzws6rr77q+XPTV01NGhoalJeXp549e/qtOAAAgJZqVti58cYbJUkhISHKysryOhYeHq6ePXvqz3/+s9+KAwAAaKlmhZ3GxkZJUq9evbRt2zbFx8e3SlEAAAD+4tOanb179/q7DgAAgFbh863neXl5ysvLU2VlpeeKT5OnnnqqxYUBAAD4g09hZ/78+VqwYIGGDh2qpKQkhYSE+LsuAAAAv/Ap7CxdulQrVqzQpEmT/F0PAACAX/n0uIi6ujpdccUV/q4FAADA73wKO7/+9a+1cuVKf9cCAADgdz59jXXixAk98cQTWr9+vfr376/w8HCv4w888IBfigMAAGgpn8LOxx9/rIEDB0qSiouLvY6xWBkAAAQTn8LOxo0b/V0HAABAq/BpzQ4AAEB74dOVnWuuueZ7v67asGGDzwUBAAD4k09hp2m9TpP6+noVFRWpuLj4lAeEAgAABJJPYefBBx887f558+bp6NGjLSoIAADAn/y6ZmfixIk8FwsAAAQVv4adgoICdezY0Z9vCQAA0CI+fY01btw4r9fGGB08eFDbt2/X7Nmz/VIYAACAP/gUdmJiYrxeh4aGqm/fvlqwYIFGjRrll8IAAAD8waews3z5cn/XAQAA0Cp8CjtNCgsLVVJSIkm65JJLNGjQIL8UBQAA4C8+hZ3KykrdfPPN2rRpk2JjYyVJ1dXVuuaaa7Rq1Sp169bNnzUCAAD4zKe7saZOnaojR45o586dOnTokA4dOqTi4mK53W7dfvvt/q4RAADAZz5d2VmzZo3Wr1+vtLQ0z7709HTl5uayQBkAAAQVn67sNDY2Kjw8/JT94eHhamxsbHFRAAAA/uJT2BkxYoSmTZumAwcOePbt379fd9xxh0aOHOm34gAAAFrKp7DzX//1X3K73erZs6cuvPBCXXjhherVq5fcbrceffRRf9cIAADgM5/W7PTo0UMffPCB1q9fr927d0uS0tLSlJGR4dfiAAAAWqpZV3Y2bNig9PR0ud1uhYSE6Nprr9XUqVM1depUXXbZZbrkkkv0zjvvtFatAAAAzdassPPQQw/pN7/5jRwOxynHYmJi9Lvf/U4PPPCA34oDAABoqWaFnY8++kijR48+4/FRo0apsLCwxUUBAAD4S7PCTkVFxWlvOW8SFhamr776qsVFAQAA+Euzws7555+v4uLiMx7/+OOPlZSU1OKiAAAA/KVZYee6667T7NmzdeLEiVOO1dTUaO7cufqnf/onvxUHAADQUs269fy+++7Tiy++qIsvvlg5OTnq27evJGn37t3Kzc1VQ0OD7r333lYpFAAAwBfNCjtOp1ObN2/WlClTNGvWLBljJEkhISHKzMxUbm6unE5nqxQKAADgi2b/qGBKSoreeOMNffPNN/r0009ljFGfPn103nnntUZ9AAAALeLTLyhL0nnnnafLLrvMn7UAAAD4nU/PxgIAAGgvCDsAAMBqhB0AAGA1wg4AALAaYQcAAFiNsAMAAKzm863nsFNZWZmqqqqaPS8+Pl7JycmtUBEAAC1D2IFHWVmZUlPTVFNzvNlzo6I6affuEgIPACDoEHbgUVVVpZqa4xp221w5knqe9Tz3wS+09an5qqqqIuwAAIIOYQencCT1VFxy30CXAQCAX7BAGQAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsFtCws2jRIl122WXq0qWLEhISdOONN6q0tNRrzIkTJ5Sdna2uXbsqOjpa48ePV0VFhdeYsrIyjR07Vp06dVJCQoLuuusunTx5si1bAQAAQSqgYSc/P1/Z2dnasmWL1q1bp/r6eo0aNUrHjh3zjLnjjjv02muv6YUXXlB+fr4OHDigcePGeY43NDRo7Nixqqur0+bNm/X0009rxYoVmjNnTiBaAgAAQSagDwJds2aN1+sVK1YoISFBhYWF+vGPf6zDhw9r2bJlWrlypUaMGCFJWr58udLS0rRlyxYNHz5ca9eu1a5du7R+/Xo5nU4NHDhQCxcu1MyZMzVv3jxFREQEojUAABAkgmrNzuHDhyVJcXFxkqTCwkLV19crIyPDMyY1NVXJyckqKCiQJBUUFKhfv35yOp2eMZmZmXK73dq5c+dpz1NbWyu32+21AQAAOwVN2GlsbNT06dP1ox/9SJdeeqkkqby8XBEREYqNjfUa63Q6VV5e7hnzj0Gn6XjTsdNZtGiRYmJiPFuPHj383A0AAAgWQRN2srOzVVxcrFWrVrX6uWbNmqXDhw97tn379rX6OQEAQGAEdM1Ok5ycHK1evVpvv/22LrjgAs/+xMRE1dXVqbq62uvqTkVFhRITEz1j3n//fa/3a7pbq2nMd0VGRioyMtLPXQAAgGAU0Cs7xhjl5OTopZde0oYNG9SrVy+v40OGDFF4eLjy8vI8+0pLS1VWViaXyyVJcrlc2rFjhyorKz1j1q1bJ4fDofT09LZpBAAABK2AXtnJzs7WypUr9corr6hLly6eNTYxMTGKiopSTEyMJk+erBkzZiguLk4Oh0NTp06Vy+XS8OHDJUmjRo1Senq6Jk2apCVLlqi8vFz33XefsrOzuXoDAAACG3Yef/xxSdLVV1/ttX/58uW69dZbJUkPPvigQkNDNX78eNXW1iozM1OPPfaYZ2yHDh20evVqTZkyRS6XS507d1ZWVpYWLFjQVm0AAIAgFtCwY4z5wTEdO3ZUbm6ucnNzzzgmJSVFb7zxhj9LAwAAlgiau7EAAABaA2EHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqQfFsLPhfSUlJm8wBACDYEXYsU3P4a0khmjhxos/vUV9b57+CAAAIMMKOZeqPH5FkNPAXM9WtV2qz5h7cUaDiV5/QyZMnW6c4AAACgLBjqeiEZMUl923WHPfBL1qnGAAAAogFygAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjQeBIuDKyspUVVXV7Hnx8fFKTk5uhYoAADYh7CCgysrKlJqappqa482eGxXVSbt3lxB4AADfi7CDgKqqqlJNzXENu22uHEk9z3qe++AX2vrUfFVVVRF2AADfi7CDoOBI6qm45L6BLgMAYCEWKAMAAKsRdgAAgNUIOwAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALBaWKALgD1KSkraZA4AAM1B2EGL1Rz+WlKIJk6c6PN71NfW+a8gAAD+AWEHLVZ//Igko4G/mKluvVKbNffgjgIVv/qETp482TrFAQDOeYQd+E10QrLikvs2a4774BetUwwAAP8fFigDAACrEXYAAIDVCDsAAMBqhB0AAGA1wg4AALAaYQcAAFgtoGHn7bff1vXXX6/u3bsrJCREL7/8stdxY4zmzJmjpKQkRUVFKSMjQ3v27PEac+jQIU2YMEEOh0OxsbGaPHmyjh492oZdAACAYBbQsHPs2DENGDBAubm5pz2+ZMkSPfLII1q6dKm2bt2qzp07KzMzUydOnPCMmTBhgnbu3Kl169Zp9erVevvtt/Xb3/62rVoAAABBLqA/KjhmzBiNGTPmtMeMMXrooYd033336YYbbpAk/eUvf5HT6dTLL7+sm2++WSUlJVqzZo22bdumoUOHSpIeffRRXXfddfrP//xPde/evc16AQAAwSlo1+zs3btX5eXlysjI8OyLiYnRsGHDVFBQIEkqKChQbGysJ+hIUkZGhkJDQ7V169Yzvndtba3cbrfXBgAA7BS0j4soLy+XJDmdTq/9TqfTc6y8vFwJCQlex8PCwhQXF+cZczqLFi3S/Pnz/Vwx2pOysjJVVVX5NDc+Pl7Jycl+rggA0FqCNuy0plmzZmnGjBme1263Wz169AhgRWhLZWVlSk1NU03NcZ/mR0V10u7dJQQeAGgngjbsJCYmSpIqKiqUlJTk2V9RUaGBAwd6xlRWVnrNO3nypA4dOuSZfzqRkZGKjIz0f9FoF6qqqlRTc1zDbpsrR1LPZs11H/xCW5+ar6qqKsIOALQTQRt2evXqpcTEROXl5XnCjdvt1tatWzVlyhRJksvlUnV1tQoLCzVkyBBJ0oYNG9TY2Khhw4YFqnS0E46kns1+SjsAoP0JaNg5evSoPv30U8/rvXv3qqioSHFxcUpOTtb06dP17//+7+rTp4969eql2bNnq3v37rrxxhslSWlpaRo9erR+85vfaOnSpaqvr1dOTo5uvvlm7sQCAACSAhx2tm/frmuuucbzumkdTVZWllasWKG7775bx44d029/+1tVV1fryiuv1Jo1a9SxY0fPnGeeeUY5OTkaOXKkQkNDNX78eD3yyCNt3gsAAAhOAQ07V199tYwxZzweEhKiBQsWaMGCBWccExcXp5UrV7ZGeQAAwAJB+zs7AAAA/kDYAQAAViPsAAAAqxF2AACA1Qg7AADAaoQdAABgNcIOAACwGmEHAABYjbADAACsRtgBAABWI+wAAACrEXYAAIDVCDsAAMBqhB0AAGC1sEAXALRESUlJm8wBALRfhB20SzWHv5YUookTJ/r8HvW1df4rCAAQtAg7aJfqjx+RZDTwFzPVrVdqs+Ye3FGg4lef0MmTJ1unOABAUCHsoF2LTkhWXHLfZs1xH/yidYr5AWVlZaqqqvJpbnx8vJKTk/1cEQCcGwg7QBsoKytTamqaamqO+zQ/KqqTdu8uIfAAgA8IO0AbqKqqUk3NcQ27ba4cST2bNdd98AttfWq+qqqqCDsA4APCDtCGHEk9m/21GwCgZQg7gMVYJwQAhB3AWqwTAoBvEXYAS7FOCAC+RdgBLMc6IQDnOp6NBQAArEbYAQAAVuNrLAB+xR1gAIINYQeA33AHGIBgRNgB4DfcAQYgGBF2AB+UlJS06vj2jjvAAAQTwg7QDDWHv5YUookTJ/o0v762zr8FAQB+EGEHaIb640ckGQ38xUx165V61vMO7ihQ8atP6OTJk61XHADgtAg7gA+iE5Kb9TWN++AXLT4nX50BgG8IO0CQ46szAGgZwg4Q5PjqDABahrADtBOB+OoMAGxA2AFwRqwTAmADwg6AU7BOCIBNCDsATtHe1gmdS8/j8rXX9tYn4E+EHQBn1B7WCZ1Lz+NqSa/tqU/A3wg7ANq1c+l5XL722t76BPyNsAPACufS87jOpV4BfwgNdAEAAACtiSs7AIJKe7nd/VxaFA20d4QdAEGhPd3ufi4tigZsQNgBEBTa0+3u59KiaMAGhB0AQaU93O7ehIXCQPtA2AFwzmsv64QA+IawA+Cc1Z7WCQHwHWEHwDkrkOuEfLk6xBUlwDeEHQDnvLZcJ9TSq0nSuXFFiWeAwZ8IOwDQhny9miQF7kGrvvI1sBw8eFA/+9m/6MSJmmbP5bZ+nA5hBwACoLlXk6TA3nnWXC39LSJJGjLpD4pL7nPW49vjbf1cwWobhB0AOEf4uuantrZWkZGRzT6Xr79F1HQFK6rr+Vbf2s9T7NsOYQcALNfidUIhIZIxPk2Niutu9RUsyferM74GwvZ4BSvQCDsAYDl/rBNqD79s3aQtr2C1ZH1RE18CYaC016/dCDsAcI5oyTqh9vDL1oG8gtXc9UVS4ALhubhw3Jqwk5ubqz/96U8qLy/XgAED9Oijj+ryyy8PdFkAgDYSyCtYvqwvamkg9OUKlj+uRLXHheNWhJ3nnntOM2bM0NKlSzVs2DA99NBDyszMVGlpqRISEgJdHgCgDXEF64e15EpUe1w4bkXYeeCBB/Sb3/xGv/rVryRJS5cu1euvv66nnnpK99xzT4CrAwDAf/xxBSsQV6ICqd2Hnbq6OhUWFmrWrFmefaGhocrIyFBBQUEAKwMAoPXY/ltN/tTuw05VVZUaGhrkdDq99judTu3evfu0c2pra1VbW+t5ffjwYUmS2+32a21Hjx6VJB36slQna5v3/aj74Jff1rZ/j8LDQlp9Huds3bmck3MGci7n5JyBnOsuL5P07d+J/v57tun9zA8tLDft3P79+40ks3nzZq/9d911l7n88stPO2fu3LlGEhsbGxsbG5sF2759+743K7T7Kzvx8fHq0KGDKioqvPZXVFQoMTHxtHNmzZqlGTNmeF43Njbq0KFD6tq1q0JCmpd0pW+TZY8ePbRv3z45HI5mz28v6NMu9GmPc6FHiT5t448+jTE6cuSIunfv/r3j2n3YiYiI0JAhQ5SXl6cbb7xR0rfhJS8vTzk5OaedExkZecoPR8XGxra4FofDYfW/mE3o0y70aY9zoUeJPm3T0j5jYmJ+cEy7DzuSNGPGDGVlZWno0KG6/PLL9dBDD+nYsWOeu7MAAMC5y4qwc9NNN+mrr77SnDlzVF5eroEDB2rNmjWnLFoGAADnHivCjiTl5OSc8Wur1hYZGam5c+c2+5kq7Q192oU+7XEu9CjRp23ass8QY3x8EAgAAEA7EBroAgAAAFoTYQcAAFiNsAMAAKxG2GmGRYsW6bLLLlOXLl2UkJCgG2+8UaWlpV5jTpw4oezsbHXt2lXR0dEaP378KT94GOwef/xx9e/f3/PbBy6XS2+++abnuA09ftfixYsVEhKi6dOne/bZ0Oe8efMUEhLitaWm/v8PDrShxyb79+/XxIkT1bVrV0VFRalfv37avn2757gxRnPmzFFSUpKioqKUkZGhPXv2BLDi5uvZs+cpn2dISIiys7Ml2fF5NjQ0aPbs2erVq5eioqJ04YUXauHChV6PA7Dhs5SkI0eOaPr06UpJSVFUVJSuuOIKbdu2zXO8Pfb59ttv6/rrr1f37t0VEhKil19+2ev42fR06NAhTZgwQQ6HQ7GxsZo8ebLn8Us+a/HzGs4hmZmZZvny5aa4uNgUFRWZ6667ziQnJ5ujR496xvz+9783PXr0MHl5eWb79u1m+PDh5oorrghg1c336quvmtdff9188sknprS01PzhD38w4eHhpri42BhjR4//6P333zc9e/Y0/fv3N9OmTfPst6HPuXPnmksuucQcPHjQs3311Vee4zb0aIwxhw4dMikpKebWW281W7duNZ9//rl56623zKeffuoZs3jxYhMTE2Nefvll89FHH5l//ud/Nr169TI1NTUBrLx5KisrvT7LdevWGUlm48aNxhg7Ps/777/fdO3a1axevdrs3bvXvPDCCyY6Oto8/PDDnjE2fJbGGPPzn//cpKenm/z8fLNnzx4zd+5c43A4zN///ndjTPvs84033jD33nuvefHFF40k89JLL3kdP5ueRo8ebQYMGGC2bNli3nnnHXPRRReZW265pUV1EXZaoLKy0kgy+fn5xhhjqqurTXh4uHnhhRc8Y0pKSowkU1BQEKgy/eK8884z//M//2Ndj0eOHDF9+vQx69atMz/5yU88YceWPufOnWsGDBhw2mO29GiMMTNnzjRXXnnlGY83NjaaxMRE86c//cmzr7q62kRGRppnn322LUpsFdOmTTMXXnihaWxstObzHDt2rLntttu89o0bN85MmDDBGGPPZ3n8+HHToUMHs3r1aq/9gwcPNvfee68VfX437JxNT7t27TKSzLZt2zxj3nzzTRMSEmL279/vcy18jdUCTU9Lj4uLkyQVFhaqvr5eGRkZnjGpqalKTk5WQUFBQGpsqYaGBq1atUrHjh2Ty+Wyrsfs7GyNHTvWqx/Jrs9yz5496t69u3r37q0JEyaorOzbJxDb1OOrr76qoUOH6l/+5V+UkJCgQYMG6cknn/Qc37t3r8rLy716jYmJ0bBhw9pdr03q6ur017/+VbfddptCQkKs+TyvuOIK5eXl6ZNPPpEkffTRR3r33Xc1ZswYSfZ8lidPnlRDQ4M6duzotT8qKkrvvvuuNX3+o7PpqaCgQLGxsRo6dKhnTEZGhkJDQ7V161afz23Njwq2tcbGRk2fPl0/+tGPdOmll0qSysvLFRERccpztpxOp8rLywNQpe927Nghl8ulEydOKDo6Wi+99JLS09NVVFRkTY+rVq3SBx984PUdeRNbPsthw4ZpxYoV6tu3rw4ePKj58+frqquuUnFxsTU9StLnn3+uxx9/XDNmzNAf/vAHbdu2TbfffrsiIiKUlZXl6ee7v6reHntt8vLLL6u6ulq33nqrJHv+nb3nnnvkdruVmpqqDh06qKGhQffff78mTJggSdZ8ll26dJHL5dLChQuVlpYmp9OpZ599VgUFBbrooous6fMfnU1P5eXlSkhI8DoeFhamuLi4FvVN2PFRdna2iouL9e677wa6lFbRt29fFRUV6fDhw/q///s/ZWVlKT8/P9Bl+c2+ffs0bdo0rVu37pT/s7JJ0/8NS1L//v01bNgwpaSk6Pnnn1dUVFQAK/OvxsZGDR06VH/84x8lSYMGDVJxcbGWLl2qrKysAFfXOpYtW6YxY8b84NOe25vnn39ezzzzjFauXKlLLrlERUVFmj59urp3727dZ/m///u/uu2223T++eerQ4cOGjx4sG655RYVFhYGujTr8DWWD3JycrR69Wpt3LhRF1xwgWd/YmKi6urqVF1d7TW+oqJCiYmJbVxly0REROiiiy7SkCFDtGjRIg0YMEAPP/ywNT0WFhaqsrJSgwcPVlhYmMLCwpSfn69HHnlEYWFhcjqdVvT5XbGxsbr44ov16aefWvNZSlJSUpLS09O99qWlpXm+smvq57t3JrXHXiXpyy+/1Pr16/XrX//as8+Wz/Ouu+7SPffco5tvvln9+vXTpEmTdMcdd2jRokWS7PosL7zwQuXn5+vo0aPat2+f3n//fdXX16t3795W9dnkbHpKTExUZWWl1/GTJ0/q0KFDLeqbsNMMxhjl5OTopZde0oYNG9SrVy+v40OGDFF4eLjy8vI8+0pLS1VWViaXy9XW5fpVY2Ojamtrrelx5MiR2rFjh4qKijzb0KFDNWHCBM+fbejzu44eParPPvtMSUlJ1nyWkvSjH/3olJ+B+OSTT5SSkiJJ6tWrlxITE716dbvd2rp1a7vrVZKWL1+uhIQEjR071rPPls/z+PHjCg31/qupQ4cOamxslGTfZylJnTt3VlJSkr755hu99dZbuuGGG6zs82x6crlcqq6u9rq6tWHDBjU2NmrYsGG+n9znpc3noClTppiYmBizadMmr9s/jx8/7hnz+9//3iQnJ5sNGzaY7du3G5fLZVwuVwCrbr577rnH5Ofnm71795qPP/7Y3HPPPSYkJMSsXbvWGGNHj6fzj3djGWNHn3feeafZtGmT2bt3r3nvvfdMRkaGiY+PN5WVlcYYO3o05tufDwgLCzP333+/2bNnj3nmmWdMp06dzF//+lfPmMWLF5vY2FjzyiuvmI8//tjccMMNQX8b7+k0NDSY5ORkM3PmzFOO2fB5ZmVlmfPPP99z6/mLL75o4uPjzd133+0ZY8tnuWbNGvPmm2+azz//3Kxdu9YMGDDADBs2zNTV1Rlj2mefR44cMR9++KH58MMPjSTzwAMPmA8//NB8+eWXxpiz62n06NFm0KBBZuvWrebdd981ffr04dbztiTptNvy5cs9Y2pqasy//uu/mvPOO8906tTJ/PSnPzUHDx4MXNE+uO2220xKSoqJiIgw3bp1MyNHjvQEHWPs6PF0vht2bOjzpptuMklJSSYiIsKcf/755qabbvL67Rkbemzy2muvmUsvvdRERkaa1NRU88QTT3gdb2xsNLNnzzZOp9NERkaakSNHmtLS0gBV67u33nrLSDpt7TZ8nm6320ybNs0kJyebjh07mt69e5t7773X1NbWesbY8lk+99xzpnfv3iYiIsIkJiaa7OxsU11d7TneHvvcuHHjaf+ezMrKMsacXU9ff/21ueWWW0x0dLRxOBzmV7/6lTly5EiL6uKp5wAAwGqs2QEAAFYj7AAAAKsRdgAAgNUIOwAAwGqEHQAAYDXCDgAAsBphBwAAWI2wAwAArEbYARD0vvjiC4WEhKioqCjQpQBoh/gFZQBBr6GhQV999ZXi4+MVFhYW6HIAtDOEHQCtrq6uThEREYEuA8A5iq+xAPjd1VdfrZycHE2fPl3x8fHKzMxUcXGxxowZo+joaDmdTk2aNElVVVWeOY2NjVqyZIkuuugiRUZGKjk5Wffff7+kU7/G2rRpk0JCQvT666+rf//+6tixo4YPH67i4mKvOt59911dddVVioqKUo8ePXT77bfr2LFjZ9XDY489pj59+qhjx45yOp362c9+5tXf7bffrrvvvltxcXFKTEzUvHnzvOaXlZXphhtuUHR0tBwOh37+85+roqLCh3+aAFqKsAOgVTz99NOKiIjQe++9p8WLF2vEiBEaNGiQtm/frjVr1qiiokI///nPPeNnzZqlxYsXa/bs2dq1a5dWrlwpp9P5vee466679Oc//1nbtm1Tt27ddP3116u+vl6S9Nlnn2n06NEaP368Pv74Yz333HN69913lZOT84O1b9++XbfffrsWLFig0tJSrVmzRj/+8Y9P6a9z587aunWrlixZogULFmjdunWSvg1uN9xwgw4dOqT8/HytW7dOn3/+uW666abm/mME4A8temY6AJzGT37yEzNo0CDP64ULF5pRo0Z5jdm3b5+RZEpLS43b7TaRkZHmySefPO377d2710gyH374oTHGmI0bNxpJZtWqVZ4xX3/9tYmKijLPPfecMcaYyZMnm9/+9rde7/POO++Y0NBQU1NT8731/+1vfzMOh8O43e4z9nfllVd67bvsssvMzJkzjTHGrF271nTo0MGUlZV5ju/cudNIMu+///73nhuA/7HSD0CrGDJkiOfPH330kTZu3Kjo6OhTxn322Weqrq5WbW2tRo4c2axzuFwuz5/j4uLUt29flZSUeM758ccf65lnnvGMMcaosbFRe/fuVVpa2hnf99prr1VKSop69+6t0aNHa/To0frpT3+qTp06ecb079/fa05SUpIqKyslSSUlJerRo4d69OjhOZ6enq7Y2FiVlJTosssua1afAFqGsAOgVXTu3Nnz56NHj+r666/Xf/zHf5wyLikpSZ9//rnfz3/06FH97ne/0+23337KseTk5O+d26VLF33wwQfatGmT1q5dqzlz5mjevHnatm2bYmNjJUnh4eFec0JCQtTY2Oi3+gH4D2EHQKsbPHiw/va3v6lnz56nvXW8T58+ioqKUl5enn7961+f9ftu2bLFE1y++eYbffLJJ54rNoMHD9auXbt00UUX+VRzWFiYMjIylJGRoblz5yo2NlYbNmzQuHHjfnBuWlqa9u3bp3379nmu7uzatUvV1dVKT0/3qR4AvmOBMoBWl52drUOHDumWW27Rtm3b9Nlnn+mtt97Sr371KzU0NKhjx46aOXOm7r77bv3lL3/RZ599pi1btmjZsmXf+74LFixQXl6eiouLdeuttyo+Pl433nijJGnmzJnavHmzcnJyVFRUpD179uiVV145qwXKq1ev1iOPPKKioiJ9+eWX+stf/qLGxkb17dv3rPrNyMhQv379NGHCBH3wwQd6//339ctf/lI/+clPNHTo0LN6DwD+Q9gB0Oq6d++u9957Tw0NDRo1apT69eun6dOnKzY2VqGh3/5naPbs2brzzjs1Z84cpaWl6aabbvKsgTmTxYsXa9q0aRoyZIjKy8v12muveX7Pp3///srPz9cnn3yiq666SoMGDdKcOXPUvXv3H6w3NjZWL774okaMGKG0tDQtXbpUzz77rC655JKz6jckJESvvPKKzjvvPP34xz9WRkaGevfureeee+6s5gPwL35UEEC7s2nTJl1zzTX65ptvPGtoAOBMuLIDAACsRtgBcM555513FB0dfcYNgF34GgvAOaempkb79+8/43Ff7+ACEJwIOwAAwGp8jQUAAKxG2AEAAFYj7AAAAKsRdgAAgNUIOwAAwGqEHQAAYDXCDgAAsBphBwAAWO3/ATRtHh0dtKgLAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sns.histplot(seq_df.recipe_sno.apply(len)[(seq_df.recipe_sno.apply(len) > 20) & (seq_df.recipe_sno.apply(len) < 100)])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((2393,), (135,), (6,), (20588,))" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "seq_df.recipe_sno.apply(len)[seq_df.recipe_sno.apply(len) > 20].shape, \\\n", + "seq_df.recipe_sno.apply(len)[seq_df.recipe_sno.apply(len) > 100].shape, \\\n", + "seq_df.recipe_sno.apply(len)[seq_df.recipe_sno.apply(len) > 1000].shape,\\\n", + "seq_df.recipe_sno.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> 총 2만 명의 사용자 중 10% 정도는 20개 이상의 인터렉션을 가지며, 100개 이상, 1000개 이상의 인터렉션을 가지는 유저는 매우 드문 편. sequence length가 길수록 더 많은 사용자의 상호작용을 캡처하나, 학습 및 추론 속도를 느리게 할 수 있음. 따라서 max_seq_length를 20, 50, 100으로 두고 실험하여, 성능과 속도 측면에서 비교해보고 결정하자." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "airflow", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/ml/Sequential/seq-train-config.yaml b/ml/Sequential/seq-train-config.yaml new file mode 100644 index 0000000..da10d6e --- /dev/null +++ b/ml/Sequential/seq-train-config.yaml @@ -0,0 +1,51 @@ +USER_ID_FIELD: user_id +ITEM_ID_FIELD: item_id +# RATING_FIELD: rating +TIME_FIELD: timestamp + +MAX_ITEM_LIST_LENGTH: 100 + +load_col: + inter: [user_id, item_id, timestamp] + +data_path: /dev/shm/data +dataset: BasketRecommendation + +# training config +train_batch_size: 1024 +stopping_step: 10 +epochs: 500 +learning_rate: 0.0001 +train_neg_sample_args: { + 'distribution': 'uniform', + 'sample_num': 4, + 'dynamic': False} + +# evaluation +eval_args: + group_by: user + order: TO + split: {'LS': 'valid_and_test'} + mode: full #pop100 +metrics: ['Recall', 'MRR', 'NDCG', 'Hit', 'Precision'] +topk: 20 +valid_metric: Recall@20 + +# model config: GRU4Rec +embedding_size: 64 +hidden_size: 128 +num_layers: 1 +dropout_prob: 0.3 +loss_type: 'BPR' + +# model config: SASRec, BERT4Rec +hidden_size: 128 +#inner_size +#n_layers +#n_heads +#hidden_dropout_prob +#attn_dropout_prob +#hidden_act +loss_type: 'BPR' + +# refers from: https://recbole.io/docs/get_started/started/sequential.html diff --git a/ml/Sequential/seq-train.py b/ml/Sequential/seq-train.py new file mode 100644 index 0000000..56bb160 --- /dev/null +++ b/ml/Sequential/seq-train.py @@ -0,0 +1,9 @@ +from recbole.quick_start import run_recbole + +run_recbole( + model='BERT4Rec', # GRU4Rec, SASRec, BERT4Rec + dataset='BasketRecommendation', + config_file_list=[ + 'seq-train-config.yaml' + ], + ) diff --git a/ml/eval/4.make-iter.ipynb b/ml/eval/4.make-iter.ipynb index 2e1c450..36c1e20 100644 --- a/ml/eval/4.make-iter.ipynb +++ b/ml/eval/4.make-iter.ipynb @@ -27,7 +27,7 @@ "metadata": {}, "outputs": [], "source": [ - "data_dir = '/dev/shm/data/4.split-by-user'" + "data_dir = '/dev/shm/data/3.preprocessed'" ] }, { @@ -39,7 +39,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "train_df shape: (213757, 4)\n" + "train_df shape: (276847, 4)\n" ] }, { @@ -71,39 +71,39 @@ " \n", " \n", " \n", - " 135368\n", - " 73629196\n", - " 6866734\n", + " 239004\n", + " 19831374\n", + " 6862680\n", " 5.0\n", - " 2019-03-10 11:43\n", + " 2017-04-04 06:06\n", " \n", " \n", - " 191771\n", - " chiy1101\n", - " 6852934\n", + " 146787\n", + " 75997033\n", + " 6837870\n", " 5.0\n", - " 2016-08-22 23:28\n", + " 2016-01-16 01:13\n", " \n", " \n", - " 93492\n", - " 52648451\n", - " 6873244\n", + " 76025\n", + " 19572058\n", + " 6866483\n", " 5.0\n", - " 2020-05-24 20:17\n", + " 2023-12-24 19:59\n", " \n", " \n", - " 68339\n", - " 40154159\n", - " 6851272\n", - " 5.0\n", - " 2017-06-09 16:11\n", + " 51968\n", + " 58491660\n", + " 6834238\n", + " 3.0\n", + " 2015-12-03 09:31\n", " \n", " \n", - " 20726\n", - " 19888774\n", - " 6874866\n", + " 98090\n", + " 92219780\n", + " 6844127\n", " 5.0\n", - " 2019-05-29 11:14\n", + " 2017-04-11 03:25\n", " \n", " \n", "\n", @@ -111,11 +111,11 @@ ], "text/plain": [ " uid recipe_sno rating datetime\n", - "135368 73629196 6866734 5.0 2019-03-10 11:43\n", - "191771 chiy1101 6852934 5.0 2016-08-22 23:28\n", - "93492 52648451 6873244 5.0 2020-05-24 20:17\n", - "68339 40154159 6851272 5.0 2017-06-09 16:11\n", - "20726 19888774 6874866 5.0 2019-05-29 11:14" + "239004 19831374 6862680 5.0 2017-04-04 06:06\n", + "146787 75997033 6837870 5.0 2016-01-16 01:13\n", + "76025 19572058 6866483 5.0 2023-12-24 19:59\n", + "51968 58491660 6834238 3.0 2015-12-03 09:31\n", + "98090 92219780 6844127 5.0 2017-04-11 03:25" ] }, "execution_count": 3, @@ -124,9 +124,9 @@ } ], "source": [ - "train_df = pd.read_csv(os.path.join(data_dir, 'train-data-over-5-240321.csv'))\n", - "print('train_df shape: ', train_df.shape)\n", - "train_df.sample(5)" + "df = pd.read_csv(os.path.join(data_dir, 'merged-data-over-5-240321.csv'))\n", + "print('train_df shape: ', df.shape)\n", + "df.sample(5)" ] }, { @@ -134,13 +134,6 @@ "execution_count": 4, "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "test_df shape: (63090, 4)\n" - ] - }, { "data": { "text/html": [ @@ -170,51 +163,51 @@ " \n", " \n", " \n", - " 19459\n", - " 39315509\n", - " 6928594\n", + " 53691\n", + " 61440273\n", + " 6883990\n", " 5.0\n", - " 2023-01-21 18:20\n", + " 2020-02-09 19:59\n", " \n", " \n", - " 44916\n", - " 80735651\n", - " 2800483\n", + " 68528\n", + " 69367549\n", + " 6839837\n", " 5.0\n", - " 2022-04-09 18:29\n", + " 2017-05-23 19:31\n", " \n", " \n", - " 23364\n", - " 45543474\n", - " 6842258\n", - " 5.0\n", - " 2016-04-22 10:08\n", + " 220028\n", + " 88015859\n", + " 6864234\n", + " 4.0\n", + " 2018-11-21 02:04\n", " \n", " \n", - " 53443\n", - " 94762408\n", - " 6948608\n", + " 276400\n", + " 15333309\n", + " 6922749\n", " 5.0\n", - " 2024-01-27 02:08\n", + " 2023-12-13 21:48\n", " \n", " \n", - " 45174\n", - " 80802273\n", - " 6971235\n", - " NaN\n", - " NaN\n", + " 178640\n", + " 39765052\n", + " 6894679\n", + " 5.0\n", + " 2022-06-28 18:09\n", " \n", " \n", "\n", "" ], "text/plain": [ - " uid recipe_sno rating datetime\n", - "19459 39315509 6928594 5.0 2023-01-21 18:20\n", - "44916 80735651 2800483 5.0 2022-04-09 18:29\n", - "23364 45543474 6842258 5.0 2016-04-22 10:08\n", - "53443 94762408 6948608 5.0 2024-01-27 02:08\n", - "45174 80802273 6971235 NaN NaN" + " uid recipe_sno rating datetime\n", + "53691 61440273 6883990 5.0 2020-02-09 19:59\n", + "68528 69367549 6839837 5.0 2017-05-23 19:31\n", + "220028 88015859 6864234 4.0 2018-11-21 02:04\n", + "276400 15333309 6922749 5.0 2023-12-13 21:48\n", + "178640 39765052 6894679 5.0 2022-06-28 18:09" ] }, "execution_count": 4, @@ -223,17 +216,118 @@ } ], "source": [ - "test_df = pd.read_csv(os.path.join(data_dir, 'test-data-over-5-240321.csv'))\n", - "print('test_df shape: ', test_df.shape)\n", - "test_df.sample(5)" + "# sort by uid, datetime\n", + "df = df.sort_values(by=['uid', 'datetime'], na_position='first')\n", + "df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "df['timestamp'] = pd.to_datetime(df.datetime).astype(int)/10**9" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
user_id:tokenitem_id:tokentimestamp:float
2402200070007068564321.503859e+09
2402190070007068859281.534357e+09
2402180070007068868361.546958e+09
2402170070007068922491.548445e+09
2402160070007068496551.549552e+09
\n", + "
" + ], + "text/plain": [ + " user_id:token item_id:token timestamp:float\n", + "240220 00700070 6856432 1.503859e+09\n", + "240219 00700070 6885928 1.534357e+09\n", + "240218 00700070 6886836 1.546958e+09\n", + "240217 00700070 6892249 1.548445e+09\n", + "240216 00700070 6849655 1.549552e+09" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# df = df[['uid', 'recipe_sno', 'rating', 'datetime']]\n", + "# df.columns = ['user_id:token', 'item_id:token', 'rating:float', 'timestamp:float']\n", + "# df.head()\n", + "\n", + "df = df[['uid', 'recipe_sno', 'timestamp']]\n", + "df.columns = ['user_id:token', 'item_id:token', 'timestamp:float']\n", + "df.head()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "df.to_csv('/dev/shm/data/BasketRecommendation/BasketRecommendation.inter', index=False, sep='\\t')" + ] } ], "metadata": { From f903ccef2f3d76bb900d95c6413408c4caa2cd6e Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Fri, 22 Mar 2024 11:45:48 +0900 Subject: [PATCH 124/187] =?UTF-8?q?feat:=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=EB=AA=85=20=EA=B8=B0=EB=B0=98=20=EC=9C=A0=EC=82=AC=20=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=20=EC=B0=BE=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=97=B0=EA=B5=AC=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/Content-Based/recipe-bert.ipynb | 472 +++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 ml/Content-Based/recipe-bert.ipynb diff --git a/ml/Content-Based/recipe-bert.ipynb b/ml/Content-Based/recipe-bert.ipynb new file mode 100644 index 0000000..28b9b05 --- /dev/null +++ b/ml/Content-Based/recipe-bert.ipynb @@ -0,0 +1,472 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/dev/shm/venvs/airflow/lib/python3.10/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], + "source": [ + "from transformers import BertTokenizer, BertModel\n", + "import torch\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(189926, 18)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_1571345/3952670458.py:1: DtypeWarning: Columns (12) have mixed types. Specify dtype option on import or set low_memory=False.\n", + " recipe_df = pd.read_csv('/dev/shm/notebooks/recipe-context/merged_recipes.csv', on_bad_lines='skip')\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
recipe_idrecipe_titlerecipe_nameauthor_idauthor_namerecipe_methodrecipe_statusrecipe_kindtime_takendifficultyrecipe_urlportiondatePublishedfood_img_urlingredientreviewsphoto_reviewscomments
649196994256[유아식]양배추어묵전 만들기양배추어묵전68314373아가맘부침일상밑반찬30분이내아무나https://www.10000recipe.com/recipe/69942562인분20221224233609https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '사각어묵', 'amount': '1장'}, {'na...000
770461974657시원하고 매콤한 가지 고추냉국가지고추냉국cggirl1004카푸치노끓이기일상국/탕15분이내초급https://www.10000recipe.com/recipe/19746572인분20100710123800https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '가지', 'amount': '2개'}, {'name...001
1021586901142고추장아찌 무침 절임고추 무침 삭힌고추 무침고추장아찌무침hancy002hancy002무침일상밑반찬15분이내아무나https://www.10000recipe.com/recipe/69011426인분이상20181202183712https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '삭힌 고추', 'amount': '30~40개 (고...931
99606938593돼지갈비강정~ 갈비 더이상 찜으로만 드시지말고 맛있게 튀겨 보세요^^돼지갈비강정homelover홈러버튀김일상메인반찬60분이내초급https://www.10000recipe.com/recipe/69385934인분20200807125649https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '돼지갈비', 'amount': '600g'}, {'...000
1810696956196양배추김치양배추김치cherimoya체리모야무침일상밑반찬30분이내초급https://www.10000recipe.com/recipe/69561962인분20210402214710https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '양배추', 'amount': '200g'}, {'n...000
\n", + "
" + ], + "text/plain": [ + " recipe_id recipe_title recipe_name \\\n", + "64919 6994256 [유아식]양배추어묵전 만들기 양배추어묵전 \n", + "77046 1974657 시원하고 매콤한 가지 고추냉국 가지고추냉국 \n", + "102158 6901142 고추장아찌 무침 절임고추 무침 삭힌고추 무침 고추장아찌무침 \n", + "9960 6938593 돼지갈비강정~ 갈비 더이상 찜으로만 드시지말고 맛있게 튀겨 보세요^^ 돼지갈비강정 \n", + "181069 6956196 양배추김치 양배추김치 \n", + "\n", + " author_id author_name recipe_method recipe_status recipe_kind \\\n", + "64919 68314373 아가맘 부침 일상 밑반찬 \n", + "77046 cggirl1004 카푸치노 끓이기 일상 국/탕 \n", + "102158 hancy002 hancy002 무침 일상 밑반찬 \n", + "9960 homelover 홈러버 튀김 일상 메인반찬 \n", + "181069 cherimoya 체리모야 무침 일상 밑반찬 \n", + "\n", + " time_taken difficulty recipe_url \\\n", + "64919 30분이내 아무나 https://www.10000recipe.com/recipe/6994256 \n", + "77046 15분이내 초급 https://www.10000recipe.com/recipe/1974657 \n", + "102158 15분이내 아무나 https://www.10000recipe.com/recipe/6901142 \n", + "9960 60분이내 초급 https://www.10000recipe.com/recipe/6938593 \n", + "181069 30분이내 초급 https://www.10000recipe.com/recipe/6956196 \n", + "\n", + " portion datePublished \\\n", + "64919 2인분 20221224233609 \n", + "77046 2인분 20100710123800 \n", + "102158 6인분이상 20181202183712 \n", + "9960 4인분 20200807125649 \n", + "181069 2인분 20210402214710 \n", + "\n", + " food_img_url \\\n", + "64919 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "77046 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "102158 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "9960 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "181069 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "\n", + " ingredient reviews \\\n", + "64919 {'재료': [{'name': '사각어묵', 'amount': '1장'}, {'na... 0 \n", + "77046 {'재료': [{'name': '가지', 'amount': '2개'}, {'name... 0 \n", + "102158 {'재료': [{'name': '삭힌 고추', 'amount': '30~40개 (고... 9 \n", + "9960 {'재료': [{'name': '돼지갈비', 'amount': '600g'}, {'... 0 \n", + "181069 {'재료': [{'name': '양배추', 'amount': '200g'}, {'n... 0 \n", + "\n", + " photo_reviews comments \n", + "64919 0 0 \n", + "77046 0 1 \n", + "102158 3 1 \n", + "9960 0 0 \n", + "181069 0 0 " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recipe_df = pd.read_csv('/dev/shm/notebooks/recipe-context/merged_recipes.csv', on_bad_lines='skip')\n", + "print(recipe_df.shape)\n", + "recipe_df.sample(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.0006476206522540357" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# nan 거의 없음\n", + "recipe_df.recipe_name.isna().sum()/recipe_df.recipe_name.shape[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "189803" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recipe_names = recipe_df.recipe_name.dropna().values.tolist()\n", + "len(recipe_names)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "recipe_names = recipe_names[:200]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "변환" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# 한국어 BERT 사전 훈련된 모델 로드\n", + "tokenizer = BertTokenizer.from_pretrained('klue/bert-base')\n", + "model = BertModel.from_pretrained('klue/bert-base')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# 텍스트를 토큰화하고 BERT 입력 형식에 맞게 변환\n", + "inputs = tokenizer(recipe_names, return_tensors=\"pt\", padding=True, truncation=True, max_length=512)\n", + "\n", + "# BERT 모델을 통해 텍스트 인코딩\n", + "with torch.no_grad():\n", + " outputs = model(**inputs)\n", + "\n", + "# 마지막 은닉층의 특징 벡터를 추출\n", + "last_hidden_states = outputs.last_hidden_state\n", + "\n", + "# 첫 번째 토큰([CLS] 토큰)의 은닉 상태를 문장의 임베딩으로 사용\n", + "sentence_embedding = last_hidden_states[:, 0, :]" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([200, 768])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sentence_embedding.shape" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "faiss" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "d = 64 # dimension\n", + "nb = 100000 # database size\n", + "nq = 10000 # nb of queries\n", + "np.random.seed(1234) # make reproducible\n", + "xb = np.random.random((nb, d)).astype('float32')\n", + "xb[:, 0] += np.arange(nb) / 1000.\n", + "xq = np.random.random((nq, d)).astype('float32')\n", + "xq[:, 0] += np.arange(nq) / 1000." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "True\n", + "100000\n" + ] + } + ], + "source": [ + "import faiss # make faiss available\n", + "index = faiss.IndexFlatL2(d) # build the index\n", + "print(index.is_trained)\n", + "index.add(xb) # add vectors to the index\n", + "print(index.ntotal)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[ 0 393 363 78]\n", + " [ 1 555 277 364]\n", + " [ 2 304 101 13]\n", + " [ 3 173 18 182]\n", + " [ 4 288 370 531]]\n", + "[[0. 7.175174 7.2076287 7.251163 ]\n", + " [0. 6.323565 6.684582 6.799944 ]\n", + " [0. 5.7964087 6.3917365 7.2815127]\n", + " [0. 7.277905 7.5279875 7.6628447]\n", + " [0. 6.763804 7.295122 7.368814 ]]\n" + ] + } + ], + "source": [ + "k = 4 # we want to see 4 nearest neighbors\n", + "D, I = index.search(xb[:5], k) # sanity check\n", + "print(I)\n", + "print(D)\n", + "D, I = index.search(xq, k) # actual search\n", + "print(I[:5]) # neighbors of the 5 first queries\n", + "print(I[-5:]) # neighbors of the 5 last queries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "airflow", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 812565f62ed58645dbaa9574cff04b08c6fdd327 Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Fri, 22 Mar 2024 13:49:54 +0900 Subject: [PATCH 125/187] =?UTF-8?q?feat:=20train=5Frecipe=20=EC=97=90=20?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=94=BC=20=EC=A0=95=EB=B3=B4=20=EC=A4=91=20?= =?UTF-8?q?{=20=EB=A0=88=EC=8B=9C=ED=94=BC,=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=EB=AA=85=20}=20=EC=BB=AC=EB=9F=BC=EC=9D=84=20=EA=B0=80?= =?UTF-8?q?=EC=A7=80=EB=8A=94=20=EB=8F=84=ED=81=90=EB=A8=BC=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EA=B0=80=ED=95=A8=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/Content-Based/update_train_recipes.py | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 ml/Content-Based/update_train_recipes.py diff --git a/ml/Content-Based/update_train_recipes.py b/ml/Content-Based/update_train_recipes.py new file mode 100644 index 0000000..8fc01ee --- /dev/null +++ b/ml/Content-Based/update_train_recipes.py @@ -0,0 +1,33 @@ +from datetime import datetime as dt +from tqdm import tqdm +from pymongo import MongoClient + +def main(): + client = MongoClient(host='10.0.7.6', port=27017) + db = client.dev + + data = [] + exist_recipes = [recipe['_id'] for recipe in db['train_recipes'].find()] + + # recipes 조회 + num_recipes = db['recipes'].count_documents({}) + for i, r in tqdm(enumerate(db['recipes'].find()), total=num_recipes): + if r['_id'] in exist_recipes: continue + data.append({ + '_id': r['_id'], + 'food_name': r['food_name'], + }) + print(str(r['_id']), r['food_name']) + + # 업데이트 데이터 개수 확인 + print(len(data)) + + # 업데이트 + print('before update: ', db['train_recipes'].count_documents({})) + db['train_recipes'].insert_many(data) + + # 결과 + print('after update: ', db['train_recipes'].count_documents({})) + +if __name__ == '__main__': + main() From 4f50fe57e205ddb097973176305d469d498d96cd Mon Sep 17 00:00:00 2001 From: GangBean Date: Fri, 22 Mar 2024 14:02:12 +0900 Subject: [PATCH 126/187] =?UTF-8?q?feat:=20ObjectId=EB=A1=9C=20=EB=8B=B4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=B6=94=EA=B0=80=20#50?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/preprocess.py | 95 ++++++++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/crawling/preprocess.py b/crawling/preprocess.py index 5524e6b..023d8ee 100644 --- a/crawling/preprocess.py +++ b/crawling/preprocess.py @@ -2,12 +2,15 @@ import multiprocessing import concurrent.futures import pandas as pd +import logging +from logging.handlers import QueueHandler, QueueListener from datetime import datetime as dt from pydantic import BaseModel from pymongo.collection import Collection from openai import OpenAI from tqdm import tqdm +from bson import ObjectId from database.data_source import data_source from database.data_source import data_source @@ -124,11 +127,25 @@ def multi_thread_run(self): pbar.update() def multi_processing_run(self): + logger = logging.getLogger(__name__) + logger.setLevel(logging.INFO) + + queue = multiprocessing.Queue() + queue_handler = QueueHandler(queue) + + logger.addHandler(queue_handler) + + listener = QueueListener(queue, logger) + listener.start() + + logging.info() + with multiprocessing.Pool(processes=8) as pool: - result = pool.imap(self.run, self.data) - + pool.map(self.run, self.data) + + listener.stop() - def run(self, data): + def run(self): # 1. 데이터 로드 # 2.1. recipe 단위 인스턴스 생성 # 'recipe_id', 'recipe_title', 'recipe_name', 'author_id', 'author_name', @@ -138,7 +155,8 @@ def run(self, data): ingredients_count = 0 - for idx, row in tqdm(data.iterrows(), total = data.shape[0]): + for idx, row in tqdm(self.data.iterrows(), total = self.data.shape[0], desc="Multi-processed"): + # logging.info(f"Processed: {idx} / {self.data.shape[0]}") if self.recipe_repository.find_one({'recipe_name': row['recipe_title']}): continue @@ -189,31 +207,38 @@ def run(self, data): self.ingredients_count = ingredients_count - def rename_ingredient_names(self, batch_size: int=500) -> int: + def rename_ingredient_names(self, batch_size: int=30) -> int: # batch_size개 ingredients 조회 + self.ingredients_count = self.ingredient_repository.count_documents({}) skip = 0 total_iter_count = self.ingredients_count // batch_size + 1 modified_count = 0 - for _ in tqdm(range(total_iter_count)): + for i in tqdm(range(total_iter_count)): # batch_size개씩 문서를 조회하여 가져옴 - batch_ingredients = self.recipe_repository.find({}, {'name':1}).skip(skip).limit(batch_size) + batch_ingredients = self.ingredient_repository\ + .find({}, {'name':1})\ + .sort({'name':1})\ + .skip(skip).limit(batch_size) - # name만 반환 - ingredient_names = [ingredient['name'] for ingredient in batch_ingredients] + ingredient_names: list[dict] = list(batch_ingredients) - # ingredients name solar 변환 - ingredient_names = self.llm_parsed_ingredients(ingredient_names) - - # name 내 space 제거 - ingredient_names = Preprocess.without_space(ingredient_names) + try: + # name 내 space 제거 + ingredient_names = Preprocess.without_space(ingredient_names) + + # ingredients name solar 변환 + ingredient_names = self.llm_parsed_ingredients(ingredient_names) - # 기존 이름 업데이트 - for name in ingredient_names: - for old_name, new_name in name.items(): - result = self.ingredient_repository.update_many({'name': old_name}, {'$set': {'name': new_name}}) + # 기존 이름 업데이트 + for ingredient in ingredient_names: + result = self.ingredient_repository.update_one({'_id': ObjectId(ingredient['_id'])}, {'$set': {'name': ingredient['name']}}) modified_count += result.modified_count - + except KeyboardInterrupt: + raise KeyboardInterrupt + except: + continue + # 다음 조회를 위해 skip 값을 업데이트 skip += batch_size @@ -260,7 +285,7 @@ def _llm_stream(self, ingredient_names: list[str]): messages=[ { "role": "system", - "content": "사용자가 한국 이커머스 사이트에서 식재료를 검색하여 구매하려고 해. 사용자가 입력하는 식재료명은 직접 검색어로 입력하였을 때에 식재료와 잘 매칭되지 않을 수 있어서, 검색이 가능한 형태로 변형하고 싶어. 검색 가능한 키워드로 바꿔주는 파이썬 딕셔너리를 반환해줘. 딕셔너리의 형태는 {<기존식재료명1>: <변형할 식재료명1>, <기존식재료명2>: <변형할 식재료명2>, ...} 으로 부탁해. 식재료명은 모두 한국어로 해주고, 변형이 어려운 식재료의 경우 변형할 식재료 명을 None으로 입력해도 좋아" + "content": "사용자가 한국 이커머스 사이트에서 식재료를 검색하여 구매하려고 해. 사용자가 입력하는 식재료명은 직접 검색어로 입력하였을 때에 식재료와 잘 매칭되지 않을 수 있어서, 검색이 가능한 형태로 변형하고 싶어. 검색 가능한 키워드로 바꿔주는 파이썬 딕셔너리를 반환해줘. 딕셔너리의 형태는 {<기존식재료명1>: <변형할 식재료명1>, <기존식재료명2>: <변형할 식재료명2>, ...} 으로 부탁해. 변환된 식재료명은 모두 한국어로 해주고, 변형이 어려운 식재료의 경우 변형할 식재료 명을 None으로 입력해도 좋아. 단위는 빼줘. 예를 들어 2공기밥 이면 밥으로 해줘." }, { "role": "user", @@ -278,29 +303,32 @@ def llm_parsed_ingredients(self, ingredients: list[dict]) -> list[dict]: ingredient_names = Preprocess._names_of(ingredients) # solar stream 생성 - stream = Preprocess._llm_stream(ingredient_names) + stream = self._llm_stream(ingredient_names) retry_count = 0 while stream is None: if retry_count == 10: raise ValueError('재시도 횟수 초과') time.sleep(2) - stream = Preprocess._llm_stream(ingredient_names) + stream = self._llm_stream(ingredient_names) print(f"재시도 {retry_count}") retry_count += 1 # 결과 파싱 - llm_dict = Preprocess._parsed_llm_output(stream) + llm_dict = self._parsed_llm_output(stream) + print("[LLM]:",llm_dict) # dict 형태로 변환: 에러시 오류.. try: llm_dict = eval(llm_dict) except: - with open(f'llm_output/error_.txt', 'a') as file: + with open(f'llm_output/error_{self.run_time}.txt', 'a') as file: file.write(str({ - 'input': ingredients, + 'input': ingredient_names, 'output': llm_dict })) + print('Error', llm_dict) + return [] # 생성된 매핑 딕셔너리로 기존 dict name replace replaced_ingredients = Preprocess._replaced_name_ingredients(ingredients, llm_dict) @@ -321,8 +349,7 @@ def _replaced_name_ingredients(ingredients: list[dict], llm_dict: dict): return ingredients - @staticmethod - def _parsed_llm_output(stream) -> str: + def _parsed_llm_output(self, stream) -> str: output = [] for chunk in stream: if chunk.choices[0].delta.content is not None: @@ -330,13 +357,13 @@ def _parsed_llm_output(stream) -> str: # print(content, end="") output.append(content) - # print("\n-----------") + print("\n-----------") output = "".join(output) - # print(output) + print(output) start_idx, end_idx = output.find('{'), output.find('}') output = output[start_idx:end_idx+1] - with open(f'llm_output/_.txt', 'a') as file: + with open(f'llm_output/llm_output_{self.run_time}.txt', 'a') as file: file.write(output) return output @@ -410,6 +437,10 @@ def insert_recipe(self, recipe: Recipe) -> None: count = sys.argv[-1] - preprocess = Preprocess(f'recipe_data_merged_{count}.csv') + preprocess = Preprocess(f'recipie_data_merged.csv') + + # preprocess.run() + + modified_count = preprocess.rename_ingredient_names() - preprocess.run() \ No newline at end of file + print("Modified Ingredients Count: ", modified_count) \ No newline at end of file From 07b667af603f1835a12b5384a6a3532d7eadd723 Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Fri, 22 Mar 2024 17:25:23 +0900 Subject: [PATCH 127/187] =?UTF-8?q?feat:=20train=5Frecipe=20=EB=8F=84?= =?UTF-8?q?=ED=81=90=EB=A8=BC=ED=8A=B8=EB=A5=BC=20BERT=20=EC=9E=84?= =?UTF-8?q?=EB=B2=A0=EB=94=A9=ED=95=98=EC=97=AC=20train=5Frecipe=EC=97=90?= =?UTF-8?q?=20{=20embedding:=20List=20}=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/Content-Based/recipe_embedding.py | 59 ++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 ml/Content-Based/recipe_embedding.py diff --git a/ml/Content-Based/recipe_embedding.py b/ml/Content-Based/recipe_embedding.py new file mode 100644 index 0000000..0e3e808 --- /dev/null +++ b/ml/Content-Based/recipe_embedding.py @@ -0,0 +1,59 @@ +import math +from datetime import datetime as dt +from tqdm import tqdm +from pymongo import MongoClient + +from transformers import BertTokenizer, BertModel +import torch + +def embedding_k_food_names(food_names): + # 한국어 BERT 사전 훈련된 모델 로드 + tokenizer = BertTokenizer.from_pretrained('klue/bert-base') + model = BertModel.from_pretrained('klue/bert-base') + + # 텍스트를 토큰화하고 BERT 입력 형식에 맞게 변환 + inputs = tokenizer(food_names, return_tensors="pt", padding=True, truncation=True, max_length=512) + + # BERT 모델을 통해 텍스트 인코딩 + with torch.no_grad(): + outputs = model(**inputs) + + # 마지막 은닉층의 특징 벡터를 추출 + last_hidden_states = outputs.last_hidden_state + + # 첫 번째 토큰([CLS] 토큰)의 은닉 상태를 문장의 임베딩으로 사용 + sentence_embedding = last_hidden_states[:, 0, :] + + return sentence_embedding.tolist() + +def main(): + client = MongoClient(host='10.0.7.6', port=27017) + db = client.dev + collection = db['recipes'] + + # 업데이트 + print('before update: ', collection.count_documents({})) + + batch_size = 512 + offset = 0 + + for offset in tqdm(range(math.ceil(collection.count_documents({})/512))): + offset *= batch_size + + # 컬렉션에서 100개씩 문서를 조회 + food_names = [recipe['food_name'] for recipe in collection.find().skip(offset).limit(batch_size)] + # food_embedding + food_embeddings = embedding_k_food_names(food_names) + + # update collection + for recipe, food_embedding in zip(collection.find().skip(offset).limit(batch_size), food_embeddings): + collection.update_one( + {'_id': recipe['_id']}, + {'$set': {'food_embedding': food_embedding}}) + + for i, recipe in zip(range(10), collection.find()): + print(i, len(recipe['food_embedding'])) + + +if __name__ == '__main__': + main() From 886952280822f6e681dd190ca0fac992e0de1abd Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Fri, 22 Mar 2024 17:29:58 +0900 Subject: [PATCH 128/187] =?UTF-8?q?feat:=20train=5Frecipe=20=EB=8F=84?= =?UTF-8?q?=ED=81=90=EB=A8=BC=ED=8A=B8=EB=A5=BC=20=EC=9D=BD=EC=96=B4=20?= =?UTF-8?q?=EC=9C=A0=EC=82=AC=ED=95=9C=20=EB=A0=88=EC=8B=9C=ED=94=BC?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20=EA=B0=80=EC=A7=80=EB=8A=94=20=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=ED=94=BC=EB=A5=BC=20{=20closest=5Frecipe:=20str=20}?= =?UTF-8?q?=20=EC=9C=BC=EB=A1=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?#57?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/Content-Based/update_closest_recipe.py | 57 +++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 ml/Content-Based/update_closest_recipe.py diff --git a/ml/Content-Based/update_closest_recipe.py b/ml/Content-Based/update_closest_recipe.py new file mode 100644 index 0000000..3c49823 --- /dev/null +++ b/ml/Content-Based/update_closest_recipe.py @@ -0,0 +1,57 @@ +import math +from datetime import datetime as dt + +import numpy as np +from tqdm import tqdm + +from pymongo import MongoClient + +import faiss + +def main(): + client = MongoClient(host='10.0.7.6', port=27017) + db = client.dev + collection = db['recipes'] + + # 업데이트 + print('before update: ', collection.count_documents({})) + + ids = list() + names = list() + embeddings = list() + for recipe in collection.find(): + if 'food_embedding' in recipe: + ids.append(recipe['_id']) + embeddings.append(recipe['food_embedding']) + names.append(recipe['food_name']) + embeddings = np.array(embeddings).astype('float32') + + d = 768 + index = faiss.IndexFlatL2(d) # build the index + index.add(embeddings) + print(f'{dt.now()} index added') + + # find + k = 10 # we want to see 4 nearest neighbors + D, I = index.search(embeddings, k) # sanity check + print(f'{dt.now()} searched') + + # update collection + for recipe_id, similar_idx in zip(ids, I): + closest_idx = similar_idx[1] + for idx in similar_idx[1:]: + if names[similar_idx[0]] == names[idx]: + continue + else: + closest_idx = idx + break + + collection.update_one( + {'_id': recipe_id}, + {'$set': {'closest_recipe': ids[closest_idx]}}) + +# for i, recipe in zip(range(10), collection.find()): +# print(recipe['food_name'], '|\t', collection.find({'_id':recipe['closest_recipe']})[0]['food_name']) + +if __name__ == '__main__': + main() From 848031d3fddb52adf129e860273e7800b93d9399 Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Fri, 22 Mar 2024 17:58:58 +0900 Subject: [PATCH 129/187] Add price db crawling script --- crawling/crawl_price_db.py | 136 +++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 crawling/crawl_price_db.py diff --git a/crawling/crawl_price_db.py b/crawling/crawl_price_db.py new file mode 100644 index 0000000..589acb4 --- /dev/null +++ b/crawling/crawl_price_db.py @@ -0,0 +1,136 @@ +import pandas as pd +import pymongo +from pymongo import MongoClient +import pprint +import argparse +from datetime import datetime +import pandas as pd +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By +import json +import time +import os +import re +from tqdm import tqdm + +def iso_format_time(current_time): + return current_time.strftime("%Y-%m-%dT%H:%M") + +def price2num(price_text): + pattern = re.compile(r'\d+') + numbers = pattern.findall(price_text) + price = ''.join(numbers) + return int(price) + +class PriceCrawler: + def __init__(self, id, query): + self.id = id + self.url = f'https://alltimeprice.com/search/?search={query}' + + def launch_crawler(self, driver): + self.driver = driver + self.driver.get(self.url) + self.driver.implicitly_wait(3) + + def crawl_price(self): + + elem = self.driver.find_element(By.XPATH, "//*[@id='page-content-wrapper']/div[6]/div/div[4]/div[1]") + try: + divs = elem.find_elements(By.XPATH, "./div")[:6] # 상위 6개만 + except: + divs = elem.find_elements(By.XPATH, "./div") + pass + + + min_price = float('inf') + min_price_document = None + + for i, div in enumerate(divs): + + a_tag = divs[i].find_element(By.TAG_NAME, "a") + item_url = a_tag.get_attribute('href') + + img_tag = a_tag.find_element(By.TAG_NAME, 'img') + img_url = img_tag.get_attribute('src') + + price_num = price2num(a_tag.find_element(By.CLASS_NAME, 'price').text) + product_name = a_tag.find_element(By.CLASS_NAME, 'title').text + + new_document = {'_id' : self.id, + 'product_name': product_name, + 'date': iso_format_time(datetime.now()), + 'price_url' : item_url, + 'img_url' : img_url} + + if price_num < min_price: + min_price = price_num + min_price_document = new_document + + return min_price_document + +def main(args): + # MongoDB 연결 설정 + # client = MongoClient('mongodb://localhost:27017/') + client = MongoClient(args.mongo_client) + db = client['dev'] # 데이터베이스 선택 + collection = db['ingredients'] # 컬렉션 선택 + new_collection = db['prices'] + + service = ChromeService(ChromeDriverManager().install()) + + options = webdriver.ChromeOptions() + options.add_argument('--headless') + options.add_argument('--no-sandbox') + options.add_argument('--disable-dev-shm-usage') + driver = webdriver.Chrome(service=service, options=options) + + + cursor = collection.find() + + + for document in tqdm(cursor): + + # crawled doc 만들기 + if document['name'] == '': + crawled_document = {'_id' : document['_id'], + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None} + else: + try: + crawler = PriceCrawler(id = document['_id'], query=document['name']) + crawler.launch_crawler(driver) + crawled_document = crawler.crawl_price() + except: + print('doc:', document) + pass + + # insert 하기 + try: + new_collection.insert_one(crawled_document) + except pymongo.errors.DuplicateKeyError: + try: + new_collection.update_one({'_id': document['_id']}, {"$set": crawled_document}, upsert=True) + except: + print('doc:', document) + print('crawled doc: ', crawled_document) + pass + except Exception as e: + # breakpoint() + print('error: ', e) + print('doc:', document) + print('crawled doc: ', crawled_document) + pass + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="parser") + arg = parser.add_argument + + arg("--mongo_client", type=str, default="mongodb://10.0.7.6:27017/") + + args = parser.parse_args() + main(args) \ No newline at end of file From e52652a94085979cbda7f2e63693048d03f4a623 Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Fri, 22 Mar 2024 18:17:30 +0900 Subject: [PATCH 130/187] Update exception --- crawling/crawl_price_db.py | 48 ++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/crawling/crawl_price_db.py b/crawling/crawl_price_db.py index 589acb4..4145ded 100644 --- a/crawling/crawl_price_db.py +++ b/crawling/crawl_price_db.py @@ -15,6 +15,18 @@ import re from tqdm import tqdm + +def log_exception(fname, log): + with open(fname, 'a+') as log_file: + log_file.write(log + "\n") + + +def create_upper_folder(fpath): + folder_path = os.path.dirname(fpath) + if not os.path.exists(folder_path): + os.makedirs(folder_path) + print(f"폴더 '{folder_path}' 생성") + def iso_format_time(current_time): return current_time.strftime("%Y-%m-%dT%H:%M") @@ -37,11 +49,20 @@ def launch_crawler(self, driver): def crawl_price(self): elem = self.driver.find_element(By.XPATH, "//*[@id='page-content-wrapper']/div[6]/div/div[4]/div[1]") - try: + divs = elem.find_elements(By.XPATH, "./div") + + if len(divs) >= 6: divs = elem.find_elements(By.XPATH, "./div")[:6] # 상위 6개만 - except: + elif len(divs) > 0: divs = elem.find_elements(By.XPATH, "./div") - pass + else: + # 검색 결과 0개인 경우 + min_price_document = {'_id' : self.id, + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None} + return min_price_document min_price = float('inf') @@ -73,6 +94,9 @@ def crawl_price(self): def main(args): # MongoDB 연결 설정 # client = MongoClient('mongodb://localhost:27017/') + + create_upper_folder(args.log_path) + client = MongoClient(args.mongo_client) db = client['dev'] # 데이터베이스 선택 collection = db['ingredients'] # 컬렉션 선택 @@ -104,8 +128,14 @@ def main(args): crawler = PriceCrawler(id = document['_id'], query=document['name']) crawler.launch_crawler(driver) crawled_document = crawler.crawl_price() - except: - print('doc:', document) + except Exception as e: + log_exception(args.log_path, str(document['_id'])) + + crawled_document = {'_id' : document['_id'], + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None} pass # insert 하기 @@ -115,14 +145,11 @@ def main(args): try: new_collection.update_one({'_id': document['_id']}, {"$set": crawled_document}, upsert=True) except: - print('doc:', document) - print('crawled doc: ', crawled_document) + log_exception(args.log_path, str(document['_id'])) pass except Exception as e: # breakpoint() - print('error: ', e) - print('doc:', document) - print('crawled doc: ', crawled_document) + log_exception(args.log_path, str(document['_id'])) pass @@ -131,6 +158,7 @@ def main(args): arg = parser.add_argument arg("--mongo_client", type=str, default="mongodb://10.0.7.6:27017/") + arg("--log_path", type=str, default = "log/price_db_error.txt") args = parser.parse_args() main(args) \ No newline at end of file From 2c3b6d84bc14a918ef808beed8ad797e35417ca6 Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Sat, 23 Mar 2024 00:25:50 +0900 Subject: [PATCH 131/187] =?UTF-8?q?fix:=20recipes=20=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=8C=20train=5Frecipes=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=B0=8F=20DB=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/Content-Based/recipe_embedding.py | 2 +- ml/Content-Based/update_closest_recipe.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ml/Content-Based/recipe_embedding.py b/ml/Content-Based/recipe_embedding.py index 0e3e808..d6ae2f8 100644 --- a/ml/Content-Based/recipe_embedding.py +++ b/ml/Content-Based/recipe_embedding.py @@ -29,7 +29,7 @@ def embedding_k_food_names(food_names): def main(): client = MongoClient(host='10.0.7.6', port=27017) db = client.dev - collection = db['recipes'] + collection = db['train_recipes'] # 업데이트 print('before update: ', collection.count_documents({})) diff --git a/ml/Content-Based/update_closest_recipe.py b/ml/Content-Based/update_closest_recipe.py index 3c49823..eba2983 100644 --- a/ml/Content-Based/update_closest_recipe.py +++ b/ml/Content-Based/update_closest_recipe.py @@ -11,7 +11,7 @@ def main(): client = MongoClient(host='10.0.7.6', port=27017) db = client.dev - collection = db['recipes'] + collection = db['train_recipes'] # 업데이트 print('before update: ', collection.count_documents({})) From 9930d57cac30a4629a928c2ba1b0b28568f8825a Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Sat, 23 Mar 2024 13:41:30 +0900 Subject: [PATCH 132/187] =?UTF-8?q?feat:=20context-aware=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A4=80=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/context-aware/preprocessing.ipynb | 532 +++++++++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 ml/context-aware/preprocessing.ipynb diff --git a/ml/context-aware/preprocessing.ipynb b/ml/context-aware/preprocessing.ipynb new file mode 100644 index 0000000..34fc765 --- /dev/null +++ b/ml/context-aware/preprocessing.ipynb @@ -0,0 +1,532 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "data_path = \"/dev/shm/notebooks/recipe-context/merged_recipes.csv\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_1465648/3398471431.py:1: DtypeWarning: Columns (12) have mixed types. Specify dtype option on import or set low_memory=False.\n", + " recipe_data = pd.read_csv(data_path, on_bad_lines='skip')\n" + ] + } + ], + "source": [ + "recipe_data = pd.read_csv(data_path, on_bad_lines='skip')\n", + "recipe_data.nunique()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
recipe_idrecipe_titlerecipe_nameauthor_idauthor_namerecipe_methodrecipe_statusrecipe_kindtime_takendifficultyrecipe_urlportiondatePublishedfood_img_urlingredientreviewsphoto_reviewscomments
06908128홍합 오일 파스타홍합 오일 파스타ottogitoday1오뚜기레시피NaNNaNNaN30분이내초급https://www.10000recipe.com/recipe/69081281인분2019-03-11T10:42:26+09:00https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '오뚜기 프레스코 올리브유', 'amount': '4...211
16926581수미네 반찬, 아삭아삭 우엉잡채 레시피수미네 반찬, 아삭아삭 우엉잡채 레시피hskim4127저녁노을NaNNaNNaN30분이내초급https://www.10000recipe.com/recipe/69265814인분2020-02-14T07:43:05+09:00https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '우엉', 'amount': '2대'}, {'name...000
26406172금요일밤과 잘 어울리는 맥주&고등어튀김 :)금요일밤과 잘 어울리는 맥주&고등어튀김 :)catmago18NaNNaNNaNNaNNaNNaNhttps://www.10000recipe.com/recipe/6406172NaN2014-12-05T22:30:10+09:00https://recipe1.ezmember.co.kr/cache/recipe/20...{}101
36918991명절음식 애호박전 만들어봤어요명절음식 애호박전 만들어봤어요79115518구름달빛NaNNaNNaNNaNNaNhttps://www.10000recipe.com/recipe/6918991NaN2019-09-11T07:16:23+09:00https://recipe1.ezmember.co.kr/cache/recipe/20...{'재료': [{'name': '애호박', 'amount': '1개'}, {'nam...000
46855114간단한 잡채 만드는법, 잡채 황금레시피, 만능간장 만드는법, 불고기양념 만드는법간단한 잡채 만드는법, 잡채 황금레시피, 만능간장 만드는법, 불고기양념 만드는법yeoniaryyeoniaryNaNNaNNaNNaNNaNhttps://www.10000recipe.com/recipe/6855114NaN2016-08-22T15:49:44+09:00https://recipe1.ezmember.co.kr/cache/recipe/20...['당면', '잡채용돼지고기', '목이버섯', '표고버섯', '빨간파프리카', '노...200
\n", + "
" + ], + "text/plain": [ + " recipe_id recipe_title \\\n", + "0 6908128 홍합 오일 파스타 \n", + "1 6926581 수미네 반찬, 아삭아삭 우엉잡채 레시피 \n", + "2 6406172 금요일밤과 잘 어울리는 맥주&고등어튀김 :) \n", + "3 6918991 명절음식 애호박전 만들어봤어요 \n", + "4 6855114 간단한 잡채 만드는법, 잡채 황금레시피, 만능간장 만드는법, 불고기양념 만드는법 \n", + "\n", + " recipe_name author_id author_name \\\n", + "0 홍합 오일 파스타 ottogitoday1 오뚜기레시피 \n", + "1 수미네 반찬, 아삭아삭 우엉잡채 레시피 hskim4127 저녁노을 \n", + "2 금요일밤과 잘 어울리는 맥주&고등어튀김 :) catmago18 NaN \n", + "3 명절음식 애호박전 만들어봤어요 79115518 구름달빛 \n", + "4 간단한 잡채 만드는법, 잡채 황금레시피, 만능간장 만드는법, 불고기양념 만드는법 yeoniary yeoniary \n", + "\n", + " recipe_method recipe_status recipe_kind time_taken difficulty \\\n", + "0 NaN NaN NaN 30분이내 초급 \n", + "1 NaN NaN NaN 30분이내 초급 \n", + "2 NaN NaN NaN NaN NaN \n", + "3 NaN NaN NaN NaN NaN \n", + "4 NaN NaN NaN NaN NaN \n", + "\n", + " recipe_url portion \\\n", + "0 https://www.10000recipe.com/recipe/6908128 1인분 \n", + "1 https://www.10000recipe.com/recipe/6926581 4인분 \n", + "2 https://www.10000recipe.com/recipe/6406172 NaN \n", + "3 https://www.10000recipe.com/recipe/6918991 NaN \n", + "4 https://www.10000recipe.com/recipe/6855114 NaN \n", + "\n", + " datePublished \\\n", + "0 2019-03-11T10:42:26+09:00 \n", + "1 2020-02-14T07:43:05+09:00 \n", + "2 2014-12-05T22:30:10+09:00 \n", + "3 2019-09-11T07:16:23+09:00 \n", + "4 2016-08-22T15:49:44+09:00 \n", + "\n", + " food_img_url \\\n", + "0 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "1 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "2 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "3 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "4 https://recipe1.ezmember.co.kr/cache/recipe/20... \n", + "\n", + " ingredient reviews photo_reviews \\\n", + "0 {'재료': [{'name': '오뚜기 프레스코 올리브유', 'amount': '4... 2 1 \n", + "1 {'재료': [{'name': '우엉', 'amount': '2대'}, {'name... 0 0 \n", + "2 {} 1 0 \n", + "3 {'재료': [{'name': '애호박', 'amount': '1개'}, {'nam... 0 0 \n", + "4 ['당면', '잡채용돼지고기', '목이버섯', '표고버섯', '빨간파프리카', '노... 2 0 \n", + "\n", + " comments \n", + "0 1 \n", + "1 0 \n", + "2 1 \n", + "3 0 \n", + "4 0 " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "recipe_data.head(5)\n", + "\n", + "# recipe_id\t\n", + "# recipe_title\t\n", + "# recipe_name\t\n", + "# author_id\t\n", + "# author_name\t\n", + "# recipe_method\t\n", + "# recipe_status\t\n", + "# recipe_kind\t\n", + "# time_taken \n", + "# difficulty \n", + "# recipe_url -> 제외\n", + "# portion \n", + "# datePublished\t\n", + "# food_img_url -> 사진 직접 가져와서 처리할 거 아니면 의미 x\n", + "# ingredient -> 재료 표준화 필요 -> 일단 재료 안 넣고 돌아가나 확인\n", + "# reviews \n", + "# photo_reviews\t\n", + "# comments \n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
recipe_methodrecipe_statusrecipe_kindtime_taken
4502굽기간식디저트5분이내
4503끓이기일상메인반찬30분이내
4504끓이기일상메인반찬10분이내
4505끓이기일상밥/죽/떡60분이내
4506조림일상메인반찬30분이내
\n", + "
" + ], + "text/plain": [ + " recipe_method recipe_status recipe_kind time_taken\n", + "4502 굽기 간식 디저트 5분이내\n", + "4503 끓이기 일상 메인반찬 30분이내\n", + "4504 끓이기 일상 메인반찬 10분이내\n", + "4505 끓이기 일상 밥/죽/떡 60분이내\n", + "4506 조림 일상 메인반찬 30분이내" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = recipe_data[[\"recipe_method\", \"recipe_status\", \"recipe_kind\", \"time_taken\"]]\n", + "df = df.dropna()\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "recipe_method 14\n", + "recipe_status 13\n", + "recipe_kind 17\n", + "time_taken 8\n", + "dtype: int64" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.nunique()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 결측값 확인" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recipe_id 결측치: 0.0%\n", + "recipe_title 결측치: 0.02%\n", + "recipe_name 결측치: 0.06%\n", + "author_id 결측치: 0.02%\n", + "author_name 결측치: 0.85%\n", + "recipe_method 결측치: 2.37%\n", + "recipe_status 결측치: 2.92%\n", + "recipe_kind 결측치: 2.37%\n", + "time_taken 결측치: 9.75%\n", + "difficulty 결측치: 1.26%\n", + "recipe_url 결측치: 0.0%\n", + "portion 결측치: 3.64%\n", + "datePublished 결측치: 0.03%\n", + "food_img_url 결측치: 0.14%\n", + "ingredient 결측치: 0.23%\n", + "reviews 결측치: 0.0%\n", + "photo_reviews 결측치: 0.0%\n", + "comments 결측치: 0.0%\n" + ] + } + ], + "source": [ + "raw = recipe_data\n", + "nan_counts = raw.isna().sum()\n", + "num_rows = raw.shape[0]\n", + "\n", + "for col, cnt in zip(recipe_data.columns, nan_counts):\n", + " rate = round(cnt / num_rows * 100, 2)\n", + " print(f'{col} 결측치: {rate}%')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## recipe_method, recipe_status, recipe_kind 결측값 확인" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "189926\n", + "recipe_method 4503\n", + "recipe_status 5543\n", + "recipe_kind 4503\n", + "dtype: int64\n", + "recipe_method 결측치: 2.37%\n", + "recipe_status 결측치: 2.92%\n", + "recipe_kind 결측치: 2.37%\n" + ] + } + ], + "source": [ + "columns = [\"recipe_method\", \"recipe_status\", \"recipe_kind\"]\n", + "raw = recipe_data[columns]\n", + "nan_counts = raw.isna().sum()\n", + "num_rows = raw.shape[0]\n", + "\n", + "for col, cnt in zip(columns, nan_counts):\n", + " rate = round(cnt / num_rows * 100, 2)\n", + " print(f'{col} 결측치: {rate}%')\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# item 만들기\n", + "\n", + "transform_data = recipe_data[[\"recipe_id\", \"recipe_title\", \"author_id\", \"recipe_method\", \"recipe_status\", \"recipe_kind\", \"time_taken\",\t\"difficulty\"]]\n", + "transform_data.columns = ['item_id:token', 'recipe_title:token_seq', 'author_id:token', 'recipe_method:token', 'recipe_status:token', 'recipe_kind:token', 'time_taken:token', 'difficulty:token']\n", + "transform_data.to_csv('/dev/shm/data/BasketRecommendation/BasketRecommendation.item', index=False, sep='\\t')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "airflow", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 319939402de010d2280e5b0e32fbbca07ecef4f8 Mon Sep 17 00:00:00 2001 From: sangwoonoel Date: Sat, 23 Mar 2024 13:45:50 +0900 Subject: [PATCH 133/187] =?UTF-8?q?feat:=20context-aware=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=EC=9D=84=20=EC=9C=84=ED=95=9C=20Recbole=20quick=20sta?= =?UTF-8?q?rt=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/context-aware/context-aware.yaml | 33 +++++++++++++++++++++++++++++ ml/context-aware/context_aware.py | 10 +++++++++ 2 files changed, 43 insertions(+) create mode 100644 ml/context-aware/context-aware.yaml create mode 100644 ml/context-aware/context_aware.py diff --git a/ml/context-aware/context-aware.yaml b/ml/context-aware/context-aware.yaml new file mode 100644 index 0000000..2616c7c --- /dev/null +++ b/ml/context-aware/context-aware.yaml @@ -0,0 +1,33 @@ +USER_ID_FIELD: user_id +ITEM_ID_FIELD: item_id + +load_col: + inter: [user_id, item_id] + user: [user_id] + item: [item_id, recipe_title, author_id, recipe_method, recipe_status, recipe_kind, time_taken] + +data_path: /dev/shm/data +dataset: BasketRecommendation + +epochs: 20 +train_batch_size: 64 + +train_neg_sample_args: { + 'distribution': 'uniform', + 'sample_num': 1, + 'dynamic': False, + 'candidate_num': 0 + } + +stopping_step: 3 +learning_rate: 0.001 + +eval_args: + group_by: user + +metrics: ['Recall', 'MRR', 'NDCG', 'Hit', 'Precision'] +topk: 20 +valid_metric: Recall@20 +embedding_size: 64 +dropout_prob: 0.5 +mlp_hidden_size: [200, 200, 200] \ No newline at end of file diff --git a/ml/context-aware/context_aware.py b/ml/context-aware/context_aware.py new file mode 100644 index 0000000..5bd4c46 --- /dev/null +++ b/ml/context-aware/context_aware.py @@ -0,0 +1,10 @@ +from recbole.quick_start import run_recbole + + +run_recbole( + model='DeepFM', + dataset='BasketRecommendation', + config_file_list=[ + 'DeepFM.yaml' + ], + ) \ No newline at end of file From a1b14f1602ad9785226aca5d6d2d0e32c6992e17 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sun, 24 Mar 2024 18:51:37 +0900 Subject: [PATCH 134/187] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=A9=20=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=EB=A7=81=20=EB=AA=A8=EB=93=88=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crawling/crawl_price_db.py | 174 +++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 crawling/crawl_price_db.py diff --git a/crawling/crawl_price_db.py b/crawling/crawl_price_db.py new file mode 100644 index 0000000..a32db0d --- /dev/null +++ b/crawling/crawl_price_db.py @@ -0,0 +1,174 @@ +import os, re, argparse +from datetime import datetime + +import pymongo +from pymongo import MongoClient + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By + +from tqdm import tqdm + +def log_exception(fname, log): + with open(fname, 'a+') as log_file: + log_file.write(log + "\n") + + +def create_upper_folder(fpath): + folder_path = os.path.dirname(fpath) + if not os.path.exists(folder_path): + os.makedirs(folder_path) + print(f"폴더 '{folder_path}' 생성") + +def iso_format_time(current_time): + return current_time.strftime("%Y-%m-%dT%H:%M") + +def price2num(price_text): + pattern = re.compile(r'\d+') + numbers = pattern.findall(price_text) + price = ''.join(numbers) + return int(price) + +class PriceCrawler: + def __init__(self, id, query): + self.id = id + self.url = f'https://alltimeprice.com/search/?search={query}' + + def launch_crawler(self, driver): + self.driver = driver + self.driver.get(self.url) + self.driver.implicitly_wait(3) + + def crawl_price(self): + + elem = self.driver.find_element(By.XPATH, "//*[@id='page-content-wrapper']/div[6]/div/div[4]/div[1]") + divs = elem.find_elements(By.XPATH, "./div") + + if len(divs) >= 6: + divs = elem.find_elements(By.XPATH, "./div")[:6] # 상위 6개만 + elif len(divs) > 0: + divs = elem.find_elements(By.XPATH, "./div") + else: + # 검색 결과 0개인 경우 + min_price_document = {'ingredient_id' : self.id, + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None, + 'price': None} + return min_price_document + + + min_price = float('inf') + min_price_document = None + + for i, div in enumerate(divs): + + a_tag = divs[i].find_element(By.TAG_NAME, "a") + item_url = a_tag.get_attribute('href') + + img_tag = a_tag.find_element(By.TAG_NAME, 'img') + img_url = img_tag.get_attribute('src') + + price_num = price2num(a_tag.find_element(By.CLASS_NAME, 'price').text) + product_name = a_tag.find_element(By.CLASS_NAME, 'title').text + + new_document = {'ingredient_id' : self.id, + 'product_name': product_name, + 'date': iso_format_time(datetime.now()), + 'price_url' : item_url, + 'img_url' : img_url, + 'price' : price_num, + } + + if price_num < min_price: + min_price = price_num + min_price_document = new_document + + return min_price_document + +def main(args): + # MongoDB 연결 설정 + # client = MongoClient('mongodb://localhost:27017/') + + create_upper_folder(args.log_path) + + client = MongoClient(args.mongo_client) + db = client['dev'] # 데이터베이스 선택 + collection = db['ingredients'] # 컬렉션 선택 + new_collection = db['prices'] + + service = ChromeService(ChromeDriverManager().install()) + + user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' + + options = webdriver.ChromeOptions() + options.add_argument('--headless') + options.add_argument('--no-sandbox') + options.add_argument('--disable-dev-shm-usage') + options.add_argument(f'--user-agent={user_agent}') + driver = webdriver.Chrome(service=service, options=options) + + skip_count = args.skip_count + + cursor = collection.find().sort({'name':1}).skip(skip_count).limit(5000) + + for document in tqdm(list(cursor)): + + # crawled doc 만들기 + if document['name'] == '': + crawled_document = { + 'ingredient_id' : document['_id'], + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None, + 'price': None + } + else: + try: + crawler = PriceCrawler(id = document['_id'], query=document['name']) + crawler.launch_crawler(driver) + crawled_document = crawler.crawl_price() + except Exception as e: + log_exception(args.log_path, str(document['_id'])) + + crawled_document = { + 'ingredient_id' : document['_id'], + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None, + 'price': None + } + pass + + # insert 하기 + try: + new_collection.insert_one(crawled_document) + except pymongo.errors.DuplicateKeyError: + try: + new_collection.update_one({'_id': document['_id']}, {"$set": crawled_document}, upsert=True) + except: + log_exception(args.log_path, str(document['_id'])) + pass + except KeyboardInterrupt: + break + except Exception as e: + # breakpoint() + log_exception(args.log_path, str(document['_id'])) + pass + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="parser") + arg = parser.add_argument + + arg("--mongo_client", type=str, default="mongodb://10.0.7.6:27017/") + arg("--log_path", type=str, default = "log/price_db_error.txt") + arg("--skip_count", type=int, default=0) + + args = parser.parse_args() + main(args) \ No newline at end of file From 7b63c5b62ae56b566419cb7b99a2886c6e4de01d Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Sun, 24 Mar 2024 19:51:26 +0900 Subject: [PATCH 135/187] =?UTF-8?q?feat:=20sasrec=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=B6=94=EB=A1=A0=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(cb=20=EB=B0=8F=20total=EC=9D=80=20=EC=95=84?= =?UTF-8?q?=EC=A7=81=20=EC=A0=81=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=8C,=20=ED=98=84=EC=9E=AC=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=9D=98=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=EA=B0=80=20=EC=9D=B8=EC=8B=9D=20=EB=B6=88=EA=B0=80?= =?UTF-8?q?=ED=95=9C=20=EB=AC=B8=EC=A0=9C=EB=A1=9C=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=201=EB=AA=85=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95=ED=95=B4=EB=91=A0.=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EB=A6=AC=EC=85=8B=20=EB=B0=8F=20=ED=95=99=EC=8A=B5?= =?UTF-8?q?=20=EB=A0=88=EC=8B=9C=ED=94=BC=EC=97=90=20=EB=8C=80=ED=95=B4?= =?UTF-8?q?=EC=84=9C=EB=A7=8C=20=ED=94=BC=EB=93=9C=EB=B0=B1=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20db=20=EB=B0=8F=20api=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=ED=9B=84=20=EB=B3=B8=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EB=B3=B5=EA=B5=AC=20=ED=95=84=EC=9A=94)=20#62?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airflow/dags/batch_inference.py | 59 +++++--------- airflow/dags/db_operations.py | 62 +++++++++++++++ airflow/dags/recbole_inference.py | 127 ++++++++++++++++-------------- 3 files changed, 153 insertions(+), 95 deletions(-) create mode 100644 airflow/dags/db_operations.py diff --git a/airflow/dags/batch_inference.py b/airflow/dags/batch_inference.py index 35838fd..7969755 100644 --- a/airflow/dags/batch_inference.py +++ b/airflow/dags/batch_inference.py @@ -7,50 +7,30 @@ from airflow.utils.dates import days_ago from airflow.operators.bash import BashOperator from airflow.operators.python import PythonOperator -from recbole_inference import inference from db_config import db_host, db_port -def get_active_users(**context): - client = MongoClient(host=db_host, port=db_port) - db = client.dev - active_users = [user['login_id'] for user in db['users'].find()] - print(active_users) +from db_operations import fetch_user_history, update_model_recommendations +from recbole_inference import sasrec_inference - user_ids = ['76017883', '94541740'] - context["ti"].xcom_push(key='user_ids', value=user_ids) +def fetch_and_push_user_history(**context): + user_id_and_feedbacks = fetch_user_history() + context["ti"].xcom_push(key='user_id_and_feedbacks', value=user_id_and_feedbacks) def batch_inference(**context): - # user_ids - user_ids = context["ti"].xcom_pull(key='user_ids') + user_id_and_feedbacks = context["ti"].xcom_pull(key='user_id_and_feedbacks') # 설정 파일과 모델 저장 경로 설정 - config_file_path = '/home/judy/train_model/recipe-dataset.yaml' # 설정 파일 경로 - model_file_path = '/home/judy/train_model/saved/MultiDAE-Mar-14-2024_23-15-20.pth' # 모델 파일 경로 + modelpath = '/home/judy/level2-3-recsys-finalproject-recsys-01/ml/Sequential/saved/BERT4Rec-Mar-24-2024_00-51-09.pth' - recommended_items = inference( - user_ids=user_ids, - modelname='MultiDAE', - config_file=config_file_path, - model_file_path=model_file_path, - k=20) + recommended_results = sasrec_inference( + modelpath, + user_id_and_feedbacks) - context["ti"].xcom_push(key='recommended_items', value=recommended_items) + context["ti"].xcom_push(key='recommended_results', value=recommended_results) -def save_results(**context): - user_ids = context["ti"].xcom_pull(key='user_ids') - recommended_items = context["ti"].xcom_pull(key='recommended_items') - - client = MongoClient(host=db_host, port=db_port) - db = client.dev - data = [{ - 'id': user_id, - 'recommended_item': recommended_item, - 'recommended_proba': recommended_proba, - 'date': dt.now() - } for user_id, recommended_item, recommended_proba in zip(user_ids, recommended_items['item_ids'], recommended_items['item_proba'])] - - db['model_recommendation_histories'].insert_many(data) - print('push data into db') +def save_results(collection_name, **context): + recommended_results = context["ti"].xcom_pull(key='recommended_results') + update_model_recommendations(recommended_results, collection_name) with DAG( dag_id="batch_inference", @@ -62,17 +42,17 @@ def save_results(**context): # get active user t1 = PythonOperator( - task_id="get_active_users", - python_callable=get_active_users, + task_id="fetch_and_push_user_history", + python_callable=fetch_and_push_user_history, depends_on_past=False, owner="judy", retries=3, retry_delay=timedelta(minutes=5), ) - # inference + # hybrid inference t2 = PythonOperator( - task_id="batch_inference", + task_id="batch_inference_by_hybrid", python_callable=batch_inference, depends_on_past=False, owner="judy", @@ -87,6 +67,9 @@ def save_results(**context): depends_on_past=False, owner="judy", retries=3, + op_kwargs={ + 'collection_name': 'model_recommendation_history_hybrid', + 'meta': {'model_version': '0.0.1'}}, retry_delay=timedelta(minutes=5), ) diff --git a/airflow/dags/db_operations.py b/airflow/dags/db_operations.py new file mode 100644 index 0000000..5c0c6f6 --- /dev/null +++ b/airflow/dags/db_operations.py @@ -0,0 +1,62 @@ +from datetime import datetime as dt + +from bson import ObjectId +from pymongo import MongoClient + +from db_config import db_host, db_port + +def fetch_user_history(): + + # db setting + client = MongoClient(host=db_host, port=db_port) + db = client.dev + + # user + user_id_and_feedbacks = [] + for u in db['users'].find({'_id':ObjectId('65fe8ede5b23f8126f66ffa2')}): # 원랜 여기가 find() + # initial_feedback이 있음 + feedbacks = u['initial_feedback_history'] + # 추가 피드백 있는 경우 + if 'feedback_history' in u: + feedbacks.append(u['feedback_history']) + # 피드백 _id를 recipe_sno 로 변경 + recipe_snos = [] + for recipe in feedbacks: + for r in db['recipes'].find({'_id': recipe}): + recipe_snos.append(r['recipe_sno']) + + user_id_and_feedbacks.append({ + '_id': str(u['_id']), + 'feedbacks': recipe_snos, + }) + + return user_id_and_feedbacks + + +def update_model_recommendations(recommended_results, collection_name, meta={}): + + # db setting + client = MongoClient(host=db_host, port=db_port) + db = client.dev + + #print(recommended_results) + for recommended_result in recommended_results: + # print(recommended_result['_id']) + recommended_items = [] + for recipe in recommended_result['recommended_items']: + for r in db['recipes'].find({'recipe_sno': recipe}): + recommended_items.append(r['_id']) + # print(r['food_name'], end=' | ') + break + recommended_result['recommended_items'] = recommended_items + + data = [{ + 'user_id': ObjectId(recommended_result['_id']), + 'recommended_recipes': recommended_result['recommended_items'], + 'datetime': dt.now(), + **meta + } for recommended_result in recommended_results] + + db[collection_name].insert_many(data) + + print('push data into db') diff --git a/airflow/dags/recbole_inference.py b/airflow/dags/recbole_inference.py index 40a2b98..2867667 100644 --- a/airflow/dags/recbole_inference.py +++ b/airflow/dags/recbole_inference.py @@ -1,80 +1,93 @@ +import math from typing import List import torch -from recbole.config import Config -from recbole.data import create_dataset, data_preparation -from recbole.model.general_recommender import MultiDAE # BPR -from recbole.trainer import Trainer -from recbole.utils import init_logger, init_seed +from recbole.quick_start import load_data_and_model from recbole.data.interaction import Interaction -def inference(user_ids: List[str], modelname: str, config_file: str, model_file_path: str, k: int=20): - - # 설정 파일 로드 - config = Config(model=modelname, dataset='recipe-dataset', config_file_list=[config_file]) +def prep_inference_data(user_id_and_feedbacks, dataset, config) -> Interaction: - # 데이터셋 생성 - dataset = create_dataset(config) + item_id_list, item_length = [], [] + for user_id_and_feedback in user_id_and_feedbacks: - # 데이터 분할 - train_data, valid_data, test_data = data_preparation(config, dataset) + # 레시피토큰 -> id 로 변환 + recipe_ids = [dataset.token2id(dataset.iid_field, item_token) for item_token in user_id_and_feedback['feedbacks']] - # 모델 초기화 - if modelname == 'MultiDAE': - model = MultiDAE(config, train_data.dataset).to(config['device']) - else: - raise ValueError(f'{modelname} Not Found. Train First') + item_id_list.append(recipe_ids) + item_length.append(len(recipe_ids)) - # 모델 파라미터 로드 - model.load_state_dict(torch.load(model_file_path)['state_dict']) - - # 추론 준비 - model.eval() - - # 사용자 ID에 대한 텐서 생성 - user_ids = [dataset.token2id(dataset.uid_field, user_id) for user_id in user_ids] - user_dict = { - dataset.uid_field: torch.tensor(user_ids, dtype=torch.int64).to(config['device']) + item_dict = { + 'item_id_list': torch.tensor(item_id_list, dtype=torch.int64).to(config['device']), + 'item_length': torch.tensor(item_length, dtype=torch.int64).to(config['device']) } # Interaction 객체 생성 - interaction = Interaction(user_dict) + interaction = Interaction(item_dict) + + return interaction + +def sasrec_inference(modelpath: str, user_id_and_feedbacks: list, k: int=20, batch_size: int=4096): + + num_users = len(user_id_and_feedbacks) + recommended_result = [] + + # 저장된 아티팩트 로드 + config, model, dataset, train_data, valid_data, test_data = load_data_and_model(modelpath) + + for i in range(math.ceil(num_users/batch_size)): - # 모델을 사용하여 추천 생성 - scores = model.full_sort_predict(interaction).view(-1, dataset.item_num) - probas = torch.sigmoid(scores) + # prep data + batch_data = user_id_and_feedbacks[i*batch_size: (i+1)*batch_size] + user_ids = [data['_id'] for data in batch_data] + inference_data = prep_inference_data(batch_data, dataset, config) - # 실제 인터렉션이 있는 위치를 매우 낮은 값으로 마스킹 - user_interactions = model.get_rating_matrix(interaction['user_id']) - masked_scores = probas.clone() - masked_scores[user_interactions >= 1] = -1e9 + # prediction + scores = model.full_sort_predict(inference_data).view(num_users, -1) + probas = torch.sigmoid(scores) - # 확률이 높은 아이템 20개 추출 - topk_proba, topk_item = torch.topk(masked_scores, k, dim=1) + # 확률이 높은 아이템 20개 추출 + topk_proba, topk_item = torch.topk(probas, k, dim=1) - item_ids = [ - dataset.id2token(dataset.iid_field, item_token).tolist()\ - for item_token in topk_item.detach().cpu().numpy()] - item_proba = topk_proba.detach().cpu().numpy() + # recipe id to token + recipe_tokens = [ + dataset.id2token(dataset.iid_field, item_token).tolist()\ + for item_token in topk_item.detach().cpu().numpy()] - print(item_ids) - print(item_proba) + for user, recommended_items in zip(user_ids, recipe_tokens): + recommended_result.append({ + '_id': user, + 'recommended_items': recommended_items + }) + + return recommended_result - return {'item_ids': item_ids, 'item_proba': item_proba.tolist()} if __name__ == '__main__': # 설정 파일과 모델 저장 경로 설정 - config_file_path = '/home/judy/train_model/recipe-dataset.yaml' # 설정 파일 경로 - model_file_path = '/home/judy/train_model/saved/MultiDAE-Mar-14-2024_23-15-20.pth' # 모델 파일 경로 - - recommended_items = inference( - user_ids=['76017883', '94541740'], - modelname='MultiDAE', - config_file=config_file_path, - model_file_path=model_file_path, - k=20) - - print(recommended_items['item_ids']) - print(recommended_items['item_proba']) + modelpath = '/home/judy/level2-3-recsys-finalproject-recsys-01/ml/Sequential/saved/BERT4Rec-Mar-24-2024_00-51-09.pth' + + from db_operations import fetch_user_history + + # 데이터 얻기 + user_id_and_feedbacks = fetch_user_history() + + recommended_items = sasrec_inference( + modelpath, + user_id_and_feedbacks) + + from bson import ObjectId + from pymongo import MongoClient + from db_config import db_host, db_port + + # 추천 결과 확인하기 + client = MongoClient(host=db_host, port=db_port) + db = client.dev + + for recommended_item in recommended_items: + print(recommended_item['_id']) + for recipe in recommended_item['recommended_items']: + for r in db['recipes'].find({'recipe_sno': recipe}): + print(r['food_name'], end=' | ') + print() From 5b25607062c48bb56d8174a0568fb8c8630127a0 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sun, 24 Mar 2024 20:13:43 +0900 Subject: [PATCH 136/187] =?UTF-8?q?feat:=20=EA=B0=80=EA=B2=A9=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=81=AC=EB=A1=A4=EB=A7=81=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#65?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airflow/dags/price_crawl.py | 203 ++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 airflow/dags/price_crawl.py diff --git a/airflow/dags/price_crawl.py b/airflow/dags/price_crawl.py new file mode 100644 index 0000000..3251a05 --- /dev/null +++ b/airflow/dags/price_crawl.py @@ -0,0 +1,203 @@ +import os, re +from datetime import datetime as dt +from datetime import timedelta +from pprint import pprint + +from pymongo import MongoClient +from pymongo.errors import DuplicateKeyError + +from airflow import DAG +from airflow.operators.python import task + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service as ChromeService +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By + +from tqdm import tqdm + +from db_config import db_port, db_host + + +def log_exception(fname, log): + with open(fname, 'a+') as log_file: + log_file.write(log + "\n") + +def create_upper_folder(fpath): + folder_path = os.path.dirname(fpath) + if not os.path.exists(folder_path): + os.makedirs(folder_path) + print(f"폴더 '{folder_path}' 생성") + +def iso_format_time(current_time): + return current_time.strftime("%Y-%m-%dT%H:%M") + +def price2num(price_text): + pattern = re.compile(r'\d+') + numbers = pattern.findall(price_text) + price = ''.join(numbers) + return int(price) + +class PriceCrawler: + def __init__(self, id, query): + self.id = id + self.url = f'https://alltimeprice.com/search/?search={query}' + + def launch_crawler(self, driver): + self.driver = driver + self.driver.get(self.url) + self.driver.implicitly_wait(3) + + def crawl_price(self, datetime: dt): + + elem = self.driver.find_element(By.XPATH, "//*[@id='page-content-wrapper']/div[6]/div/div[4]/div[1]") + divs = elem.find_elements(By.XPATH, "./div") + + if len(divs) >= 6: + divs = elem.find_elements(By.XPATH, "./div")[:6] # 상위 6개만 + elif len(divs) > 0: + divs = elem.find_elements(By.XPATH, "./div") + else: + # 검색 결과 0개인 경우 + min_price_document = {'ingredient_id' : self.id, + 'product_name': None, + 'date': datetime, + 'price_url' : None, + 'img_url' : None, + 'price': None} + return min_price_document + + + min_price = float('inf') + min_price_document = None + + for i, div in enumerate(divs): + + a_tag = divs[i].find_element(By.TAG_NAME, "a") + item_url = a_tag.get_attribute('href') + + img_tag = a_tag.find_element(By.TAG_NAME, 'img') + img_url = img_tag.get_attribute('src') + + price_num = price2num(a_tag.find_element(By.CLASS_NAME, 'price').text) + product_name = a_tag.find_element(By.CLASS_NAME, 'title').text + + new_document = {'ingredient_id' : self.id, + 'product_name': product_name, + 'date': datetime, + 'price_url' : item_url, + 'img_url' : img_url, + 'price' : price_num, + } + + if price_num < min_price: + min_price = price_num + min_price_document = new_document + + return min_price_document + + +# @task(task_id='ingredients', +# depends_on_past=False, +# owner='charlie', +# retries=3, +# retry_delay=timedelta(minutes=3)) +# def get_ingredients(**context): +# client = MongoClient(host=db_host, port=db_port) +# db = client.dev + +# return list(db['ingredients'].find({})) + +@task(task_id='crawl_price', + depends_on_past=False, + owner='charlie', + retries=3, + retry_delay=timedelta(minutes=3)) +def crawl_price(**context): + + log_path = "../logs/crawl_price/price_db_error.txt" + create_upper_folder(log_path) + + # db connection + client = MongoClient(host=db_host, port=db_port) + db = client.dev + new_collection = db['prices'] + + # chrome setting + service = ChromeService(ChromeDriverManager().install()) + user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' + + options = webdriver.ChromeOptions() + options.add_argument('--headless') + options.add_argument('--no-sandbox') + options.add_argument('--disable-dev-shm-usage') + options.add_argument(f'--user-agent={user_agent}') + driver = webdriver.Chrome(service=service, options=options) + + # execution time + datetime: dt = context['logical_date'] + print(datetime) + + # crawling + ingredients = list(db['ingredients'].find({})) + print([ingredient['_id'] for ingredient in ingredients]) + for document in tqdm(ingredients): + # crawled doc 만들기 + if document['name'] == '': + crawled_document = { + 'ingredient_id' : document['_id'], + 'product_name': None, + 'date': datetime, + 'price_url' : None, + 'img_url' : None, + 'price': None + } + else: + try: + crawler = PriceCrawler(id = document['_id'], query=document['name']) + crawler.launch_crawler(driver) + crawled_document = crawler.crawl_price(datetime) + except Exception as e: + log_exception(log_path, str(document['_id'])) + + crawled_document = { + 'ingredient_id' : document['_id'], + 'product_name': None, + 'date': datetime, + 'price_url' : None, + 'img_url' : None, + 'price': None + } + pass + + # insert 하기 + try: + new_collection.insert_one(crawled_document) + except DuplicateKeyError: + try: + new_collection.update_one({'_id': document['_id']}, {"$set": crawled_document}, upsert=True) + except: + log_exception(log_path, str(document['_id'])) + pass + except KeyboardInterrupt: + break + except Exception as e: + # breakpoint() + log_exception(log_path, str(document['_id'])) + pass + + +with DAG( + dag_id='price_crawl', + description="Crawl ingredients price once per a day at 2:00AM KST", + start_date=dt(2024,3,24), + catchup=False, + schedule_interval="0 2 * * *", # 매일 2시에 시작 + tags=["basket_recommendation", "crawling", "price"], +) as dag: + + # get_ingredient_names_task = get_ingredients() + + crawl_price_task = crawl_price() + + crawl_price_task \ No newline at end of file From 5012934aef2a3551914a7dae1a43878f76df9db8 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sun, 24 Mar 2024 22:13:57 +0900 Subject: [PATCH 137/187] =?UTF-8?q?fix:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=B6=94=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/routes/recipes/entity/ingredient.py | 8 ++-- .../app/api/routes/recipes/entity/recipe.py | 2 +- backend/app/api/routes/recipes/entity/user.py | 8 ++-- .../recipes/repository/recipes_repository.py | 41 ++++++++++++++++++- .../controller/response/signup_response.py | 1 + .../users/controller/user_controller.py | 19 +++++++-- .../api/routes/users/service/user_service.py | 38 ++++++++++------- 7 files changed, 89 insertions(+), 28 deletions(-) diff --git a/backend/app/api/routes/recipes/entity/ingredient.py b/backend/app/api/routes/recipes/entity/ingredient.py index dd60410..93ab91a 100644 --- a/backend/app/api/routes/recipes/entity/ingredient.py +++ b/backend/app/api/routes/recipes/entity/ingredient.py @@ -7,6 +7,7 @@ class Ingredient(BaseModel): name: str price: float price_url: str + amount: dict model_config = ConfigDict( populate_by_name=True, @@ -16,6 +17,7 @@ class Ingredient(BaseModel): "id": "recipe_id", "name": "김치", "price": "5800", + "amount": { "value": 240, "unit": 'g' }, "price_url": "https://www.10000recipe.com/recipe/view.html?seq=6908832&targetList=reviewLists#reviewLists", } }, @@ -45,9 +47,9 @@ def as_basket_form(self): return { 'ingredient_id': self.id, 'ingredient_name': self.name, - 'ingredient_amount': 0, - 'ingredient_unit': 'None', + 'ingredient_amount': self.amount['value'], + 'ingredient_unit': self.amount['unit'], 'ingredient_price': self.price, - 'img_link': 'None', + 'img_link': 'https://upload.wikimedia.org/wikipedia/commons/1/14/No_Image_Available.jpg', 'market_url': self.price_url, } diff --git a/backend/app/api/routes/recipes/entity/recipe.py b/backend/app/api/routes/recipes/entity/recipe.py index ecdc305..3c1e35f 100644 --- a/backend/app/api/routes/recipes/entity/recipe.py +++ b/backend/app/api/routes/recipes/entity/recipe.py @@ -8,7 +8,7 @@ class Recipe(BaseModel): food_name: str recipe_name: str ingredients: List[PyObjectId] = [] - time_taken: int + time_taken: str difficulty: str recipe_url: str portion: str diff --git a/backend/app/api/routes/recipes/entity/user.py b/backend/app/api/routes/recipes/entity/user.py index d7f9635..3c0a8e7 100644 --- a/backend/app/api/routes/recipes/entity/user.py +++ b/backend/app/api/routes/recipes/entity/user.py @@ -5,10 +5,10 @@ class User(BaseModel): id: PyObjectId = Field(alias='_id', default=None) - user_nickname: str - user_name: str - user_email: str - user_password: str + nickname: str + # user_name: str + email: str + password: str allergy: List[PyObjectId] = [] recommend_history_by_model: List[PyObjectId] = [] recommend_history_by_basket: List[PyObjectId] = [] diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index 591cf77..f932ff9 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -6,13 +6,18 @@ from ..entity.recipes import Recipes from ..entity.ingredients import Ingredients +import logging + +logging.basicConfig(level=logging.DEBUG) + class RecipesRepository: def __init__(self): self.users_collection = data_source.collection_with_name_as("users") self.recipes_collection = data_source.collection_with_name_as("recipes") self.ingredients_collection = data_source.collection_with_name_as("ingredients") - + self.amounts_collection = data_source.collection_with_name_as("amounts") + self.prices_collection = data_source.collection_with_name_as("prices") def select_user_by_user_id(self, login_id: str) -> User: user = self.users_collection.find_one({"login_id": login_id}) @@ -29,7 +34,39 @@ def select_recipes_by_recipes_id(self, recipes_id: List[str]) -> Recipes: def select_ingredients_by_ingredients_id(self, ingredients_id: List[str]) -> Ingredients: - ingredients = Ingredients(ingredients = self.ingredients_collection.find({"_id": { "$in": list(map(ObjectId, ingredients_id))} })) + # logging.debug("----------[Recipe Repository]-----------") + # ingredients = list(self.ingredients_collection.find({"_id": { "$in": list(map(ObjectId, ingredients_id))} })) + amounts = list(self.amounts_collection + .find({"_id": { "$in": list(map(ObjectId, ingredients_id))} }) + .sort({'ingredient_id': 1})) + ingredient_ids = [amount['ingredient_id'] for amount in amounts] + ingredients = list(self.ingredients_collection + .find({"_id": {"$in": list(map(ObjectId, ingredient_ids))}}) + .sort({'_id': 1})) + prices = list(next(self.prices_collection + .find({"ingredient_id": ingredient_id}).sort({"date": -1}).limit(1)) for ingredient_id in ingredient_ids) + # prices = list(self.prices_collection.find({"ingredient_id": {"$in": list(map(ObjectId, ingredient_ids))}})) + + # logging.debug(len(amounts), len(ingredients), len(prices)) + ingredient_list = list() + for amount, ingredient, price in zip(amounts, ingredients, prices): + # id: PyObjectId = Field(alias='_id', default=None) + # name: str + # price: float + # price_url: str + # amount: dict + amount['name'] = ingredient['name'] + amount['price'] = price['price'] + amount['price_url'] = price['price_url'] + amount['amount'] = { + 'value': amount['value'], + 'unit': amount['unit'], + } + ingredient_list.append(amount) + + # logging.debug('[RECIPE_REPOSITORY_RESULT]', ingredient_list) + ingredients = Ingredients(ingredients = ingredient_list) + # logging.debug('ingredients', ingredients.get_ingredients()) if ingredients: return ingredients raise HTTPException(status_code=404, detail=f"Ingredients not found") diff --git a/backend/app/api/routes/users/controller/response/signup_response.py b/backend/app/api/routes/users/controller/response/signup_response.py index fc52c23..b4e4393 100644 --- a/backend/app/api/routes/users/controller/response/signup_response.py +++ b/backend/app/api/routes/users/controller/response/signup_response.py @@ -11,6 +11,7 @@ class LoginResponse(BaseModel): token: str login_id: str password: str + is_first_login: bool class FavorRecipesResponse(BaseModel): recipes: list \ No newline at end of file diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 5f14a55..7b072d1 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -32,8 +32,12 @@ async def sign_up(self, signup_request: SignupRequest) -> SignupResponse: ) async def login(self, login_request: UserLoginDTO) -> LoginResponse: + user_login_dto, is_first_login = self.user_service.login(login_request) return LoginResponse( - **dict(self.user_service.login(login_request)) + token=user_login_dto.token, + login_id=user_login_dto.login_id, + password=user_login_dto.password, + is_first_login=is_first_login ) async def is_login_id_usable(self, login_id: str) -> bool: @@ -59,8 +63,15 @@ async def recommended_basket(self, user_id: str, price: int): # recipe 정보 가져오기 recipe_infos = self.recipe_service.get_recipes_by_recipes_id(top_k_recipes) + # logging.debug(recipe_infos.get_total_ingredients_set()) + # ingredient 정보 가져오기 - price_infos = self.recipe_service.get_prices_by_ingredients_id(recipe_infos.get_total_ingredients_set()) + price_infos = recipe_infos.get_total_ingredients_set() + + # logging.debug("----------[user_controller]------------") + # logging.debug(recipe_infos) + # logging.debug(price_infos) + price_infos = self.recipe_service.get_prices_by_ingredients_id(price_infos) # 장바구니 추천 recipes = recipe_infos.get_recipes() @@ -76,7 +87,7 @@ async def recommended_basket(self, user_id: str, price: int): return recommended_basket def _basket_with_infos(self, recommended_basket: dict, recipe_infos: Recipes): - logging.debug('recommended_basket', recommended_basket) + # logging.debug('recommended_basket', recommended_basket) # logging.debug(recipe_infos) recipe_info_list = [recipe.as_basket_form() for recipe in recipe_infos.get_recipes() if recipe.get_id() in recommended_basket['recipe_list']] @@ -154,7 +165,7 @@ async def favor_recipes(page_num: int=1) -> Response: @user_router.post('/api/users/{user_id}/foods') async def save_favor_recipes(user_id: str, request: UserFavorRecipesRequest) -> Response: await user_controller.save_favor_recipes(user_id, request) - return Response(status_code=status.HTTP_200_OK) + return Response(status_code=status.HTTP_201_CREATED) @user_router.post('/api/users/{user_id}/recommendations') async def get_recommendation(user_id: str, price: int) -> JSONResponse: diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index cd53d51..59a7a18 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -1,5 +1,6 @@ import uuid import pulp +import logging from fastapi import HTTPException from datetime import datetime, timedelta @@ -13,6 +14,8 @@ ) from ..controller.request.signup_request import UserFavorRecipesRequest +logging.basicConfig(level=logging.DEBUG) + class UserService: def __init__(self, user_repository :UserRepository, @@ -30,15 +33,18 @@ def __init__(self, def sign_up(self, sign_up_request: UserSignupDTO) -> UserSignupDTO: return self.user_repository.insert_one(sign_up_request) - def login(self, login_request: UserLoginDTO) -> UserLoginDTO: + def login(self, login_request: UserLoginDTO): user = self.user_repository.find_one({'login_id': login_request.login_id, 'password': login_request.password}) if user is None: raise HTTPException(status_code=400, detail="아이디와 비밀번호가 일치하지 않습니다.") + + if_first_login: bool = ('initial_feedback_history' not in user) user = User(**dict(user)) token = str(uuid.uuid4()) expire_date = datetime.now() + timedelta(seconds=30 * 60) - return self.session_repository.insert_one(login_id=login_request.login_id, token=token, expire_date=expire_date) + + return self.session_repository.insert_one(login_id=login_request.login_id, token=token, expire_date=expire_date), if_first_login def is_login_id_usable(self, login_id: str) -> bool: if self.user_repository.find_one({'login_id': login_id}) is not None: @@ -84,18 +90,22 @@ def _optimized_results(self, # 가격 상한 MAX_PRICE = price + logging.debug("-----------[OPTIMIZING]------------") + logging.debug(recipe_requirement_infos) + logging.debug(price_infos) + # 문제 인스턴스 생성 prob = pulp.LpProblem("MaximizeNumberOfDishes", pulp.LpMaximize) # ingredients_price = {'신김치': 7000, '돼지고기': 5000, '양파': 3000, '두부': 1500, '애호박': 2000, '청양고추': 1000, '계란': 6000, '밀가루': 3000} # 식재료 가격 및 판매단위 변수 (0 또는 1의 값을 가짐) - x = pulp.LpVariable.dicts("ingredient", price_infos.keys(), cat='Binary') # 재료 포함 여부 + ingredient_use_variable = pulp.LpVariable.dicts("ingredient", price_infos.keys(), cat='Binary') # 재료 포함 여부 # 요리 변수 # dishes = ['Dish1', 'Dish2'] dishes = recipe_requirement_infos.keys() # dishes = ['돼지고기김치찌개', '된장찌개', '애호박전'] - y = pulp.LpVariable.dicts("dish", dishes, cat='Binary') # 요리 포함 여부 + dish_use_variable = pulp.LpVariable.dicts("dish", dishes, cat='Binary') # 요리 포함 여부 # 요리별 필요한 식재료 양 # requirements = {'Dish1': {'A': 5, 'B': 2}, 'Dish2': {'B': 2, 'C': 2}} @@ -107,15 +117,15 @@ def _optimized_results(self, # print(a) # 목표 함수 (최대화하고자 하는 요리의 수) - prob += pulp.lpSum([y[dish] for dish in dishes]) + prob += pulp.lpSum([dish_use_variable[dish] for dish in dishes]) # 비용 제약 조건 - prob += pulp.lpSum([price_infos[i]*x[i] for i in price_infos]) <= MAX_PRICE + prob += pulp.lpSum([price_infos[i]*ingredient_use_variable[i] for i in price_infos]) <= MAX_PRICE # 요리별 식재료 제약 조건 for dish in dishes: for ingredient in recipe_requirement_infos[dish]: - prob += (x[ingredient] >= y[dish]) + prob += (ingredient_use_variable[ingredient] >= dish_use_variable[dish]) # 정량 제약 조건: 요리에 사용되는 재료의 총합은 각 재료의 상한을 넘지 못함 # for ingredient in price_infos.keys(): @@ -127,12 +137,12 @@ def _optimized_results(self, # 결과 출력 # print("Status:", pulp.LpStatus[prob.status]) - - # for dish in dishes: - # print(f"Make {dish}:", y[dish].varValue) - # for ingredient in price_infos: - # print(f"Use Ingredient {ingredient}:", x[ingredient].varValue) - result_dish = [dish for dish in dishes if y[dish].varValue == 1] - result_ingredient = [ingredient for ingredient in price_infos if x[ingredient].varValue == 1] + + for dish in dishes: + print(f"Make {dish}:", dish_use_variable[dish].varValue) + for ingredient in price_infos: + print(f"Use Ingredient {ingredient}:", ingredient_use_variable[ingredient].varValue) + result_dish = [dish for dish in dishes if dish_use_variable[dish].varValue == 1] + result_ingredient = [ingredient for ingredient in price_infos if ingredient_use_variable[ingredient].varValue == 1] return {'recipe_list' : result_dish, 'ingredient_list': result_ingredient} From d4a9228dfd190fe76cc01835dd17b7bb9d5904cb Mon Sep 17 00:00:00 2001 From: GangBean Date: Sun, 24 Mar 2024 22:21:24 +0900 Subject: [PATCH 138/187] =?UTF-8?q?feat:=20=EC=9E=AC=EB=A3=8C=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=A0=9C=EA=B1=B0=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/routes/recipes/entity/ingredient.py | 3 ++- .../app/api/routes/recipes/repository/recipes_repository.py | 1 + backend/app/api/routes/users/service/user_service.py | 6 +++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/recipes/entity/ingredient.py b/backend/app/api/routes/recipes/entity/ingredient.py index 93ab91a..c252646 100644 --- a/backend/app/api/routes/recipes/entity/ingredient.py +++ b/backend/app/api/routes/recipes/entity/ingredient.py @@ -8,6 +8,7 @@ class Ingredient(BaseModel): price: float price_url: str amount: dict + img_url: str model_config = ConfigDict( populate_by_name=True, @@ -50,6 +51,6 @@ def as_basket_form(self): 'ingredient_amount': self.amount['value'], 'ingredient_unit': self.amount['unit'], 'ingredient_price': self.price, - 'img_link': 'https://upload.wikimedia.org/wikipedia/commons/1/14/No_Image_Available.jpg', + 'img_link': self.img_url if self.img_url else 'https://upload.wikimedia.org/wikipedia/commons/1/14/No_Image_Available.jpg', 'market_url': self.price_url, } diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index f932ff9..bdb8e39 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -62,6 +62,7 @@ def select_ingredients_by_ingredients_id(self, ingredients_id: List[str]) -> Ing 'value': amount['value'], 'unit': amount['unit'], } + amount['img_url'] = price['img_url'] ingredient_list.append(amount) # logging.debug('[RECIPE_REPOSITORY_RESULT]', ingredient_list) diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index 59a7a18..0e2949c 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -90,9 +90,9 @@ def _optimized_results(self, # 가격 상한 MAX_PRICE = price - logging.debug("-----------[OPTIMIZING]------------") - logging.debug(recipe_requirement_infos) - logging.debug(price_infos) + # logging.debug("-----------[OPTIMIZING]------------") + # logging.debug(recipe_requirement_infos) + # logging.debug(price_infos) # 문제 인스턴스 생성 prob = pulp.LpProblem("MaximizeNumberOfDishes", pulp.LpMaximize) From 782feeee9256767536bf0e328332ec86ecf36a3b Mon Sep 17 00:00:00 2001 From: GangBean Date: Sun, 24 Mar 2024 22:34:15 +0900 Subject: [PATCH 139/187] =?UTF-8?q?fix:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=B6=94=EC=B2=9C=20=EA=B2=B0=EA=B3=BC=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=BD=9C=EB=A0=89=EC=85=98=20=EB=AF=B8=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/routes/users/repository/user_repository.py | 6 ++++++ backend/app/api/routes/users/service/user_service.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index 82c8d34..f03504f 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -33,6 +33,12 @@ def update_food(self, login_id: str, foods: list) -> int: result = self.collection.update_one(query, update_value) return result.modified_count + def update_recommended_basket(self, login_id: str, recipe_list: list): + query = {'login_id': login_id} + update_value = {'$addToSet': {'recommend_history_by_basket': {'$each': recipe_list}}} + result = self.collection.update_one(query, update_value) + return result.modified_count + class SessionRepository: def __init__(self): self.collection = data_source.collection_with_name_as('sessions') diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index 0e2949c..b088499 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -74,6 +74,7 @@ def recommended_basket(self, recipe_infos: dict, price_infos: dict, price: int) return self._optimized_results(recipe_infos, price_infos, price) def save_basket(self, user_id, price, datetime, recommended_basket) -> None: + # 추천 결과 정보 저장 self.basket_repository.save({ 'user_id': user_id, 'price': price, @@ -81,6 +82,9 @@ def save_basket(self, user_id, price, datetime, recommended_basket) -> None: 'ingredients': recommended_basket['ingredient_list'], 'recipes': recommended_basket['recipe_list'], 'basket_price': 0}) + + # 유저 recommend 정보 append + self.user_repository.update_recommended_basket(user_id, recommended_basket['recipe_list']) def _optimized_results(self, recipe_requirement_infos: dict, From c2d08a222a129c596dba393b451e071affeba192 Mon Sep 17 00:00:00 2001 From: GangBean Date: Sun, 24 Mar 2024 22:45:35 +0900 Subject: [PATCH 140/187] =?UTF-8?q?fix:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EA=B0=80=EA=B2=A9=20=EC=A0=95=EB=B3=B4=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=20=EC=88=98=EC=A0=95=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../users/controller/user_controller.py | 19 +++++++++++++------ .../api/routes/users/service/user_service.py | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 7b072d1..11cd1e8 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -80,13 +80,20 @@ async def recommended_basket(self, user_id: str, price: int): recommended_basket = self.user_service.recommended_basket(recipes, price_infos, price) # 추천 장바구니 결과 저장 - self.user_service.save_basket(user_id, price, dt.now(), recommended_basket) + basket_price = sum([price for id, price in price_infos.items() if id in recommended_basket['ingredient_list']]) + recommended_basket['basket_price'] = basket_price + self.user_service.save_basket( + user_id=user_id, + price=price, + datetime=dt.now(), + recommended_basket=recommended_basket, + ) - recommended_basket = self._basket_with_infos(recommended_basket, recipe_infos) + recommended_basket = self._basket_info(recommended_basket, recipe_infos) return recommended_basket - def _basket_with_infos(self, recommended_basket: dict, recipe_infos: Recipes): + def _basket_info(self, recommended_basket: dict, recipe_infos: Recipes): # logging.debug('recommended_basket', recommended_basket) # logging.debug(recipe_infos) @@ -97,12 +104,12 @@ def _basket_with_infos(self, recommended_basket: dict, recipe_infos: Recipes): ingredient_info_list = [ingredient.as_basket_form() for ingredient in total_ingredients if ingredient.get_id() in recommended_basket['ingredient_list']] # logging.debug('Ingredient Basket Form', ingredient_info_list) - basket_with_infos = { - 'basket_price': 0, + basket_info = { + 'basket_price': recommended_basket['basket_price'], 'ingredient_list': ingredient_info_list, 'recipe_list': recipe_info_list, } - return basket_with_infos + return basket_info user_controller = UserController( diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index b088499..91a15b9 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -73,7 +73,7 @@ def recommended_basket(self, recipe_infos: dict, price_infos: dict, price: int) # 이진 정수 프로그래밍 return self._optimized_results(recipe_infos, price_infos, price) - def save_basket(self, user_id, price, datetime, recommended_basket) -> None: + def save_basket(self, user_id, price, datetime, recommended_basket, basket_price: float = 0.) -> None: # 추천 결과 정보 저장 self.basket_repository.save({ 'user_id': user_id, @@ -81,7 +81,7 @@ def save_basket(self, user_id, price, datetime, recommended_basket) -> None: 'datetime': datetime, 'ingredients': recommended_basket['ingredient_list'], 'recipes': recommended_basket['recipe_list'], - 'basket_price': 0}) + 'basket_price': recommended_basket['basket_price']}) # 유저 recommend 정보 append self.user_repository.update_recommended_basket(user_id, recommended_basket['recipe_list']) From af595e8d2ade1eaf769db09470ae021bead725ae Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 25 Mar 2024 00:02:39 +0900 Subject: [PATCH 141/187] =?UTF-8?q?feat:=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=AF=EC=88=98=20=EC=B4=88=EA=B3=BC=EC=8B=9C=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8D=94=EB=B3=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/main_2.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/frontend/main_2.py b/frontend/main_2.py index d4f283d..6408bad 100644 --- a/frontend/main_2.py +++ b/frontend/main_2.py @@ -2,6 +2,8 @@ import requests from streamlit_extras.stylable_container import stylable_container +from streamlit_extras.switch_page_button import switch_page + def get_response(formatted_url): response = requests.get(formatted_url) @@ -44,11 +46,16 @@ def display_my_recipe_container(my_recipe_list): container3_4 = st.container(border = True) with container3_4: - st.markdown("

내가 해먹은 레시피

", unsafe_allow_html=True) - + cols = st.columns([6,1]) + with cols[0]: + st.markdown("

내가 요리한 레시피

", unsafe_allow_html=True) + with cols[1]: + if st.button("더보기", key='more_user_history'): + switch_page("user_history") + cols = st.columns((1, 1, 1, 1, 1)) - for i, my_recipe in enumerate(my_recipe_list): + for i, my_recipe in enumerate(my_recipe_list[:5]): with cols[i]: with st.container(border=True): st.image(my_recipe["recipe_img_url"]) @@ -59,11 +66,16 @@ def display_recommended_container(recommended_list): container3_4 = st.container(border = True) with container3_4: - st.markdown("

내가 좋아할 레시피

", unsafe_allow_html=True) - + cols = st.columns([6,1]) + with cols[0]: + st.markdown("

내가 좋아할 레시피

", unsafe_allow_html=True) + with cols[1]: + if st.button("더보기", key='more_recommendation_history'): + switch_page("recommendation_history") + cols = st.columns((1, 1, 1, 1, 1)) - for i, recommended in enumerate(recommended_list): + for i, recommended in enumerate(recommended_list[:5]): with cols[i]: with st.container(border=True): st.image(recommended["recipe_img_url"]) From 35988165bc66027c0e40a541050b54d5d364924d Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 25 Mar 2024 00:21:23 +0900 Subject: [PATCH 142/187] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EB=A0=88=EC=8B=9C=ED=94=BC=20=EB=A7=81?= =?UTF-8?q?=ED=81=AC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/main_2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/main_2.py b/frontend/main_2.py index 6408bad..eca1ca5 100644 --- a/frontend/main_2.py +++ b/frontend/main_2.py @@ -58,7 +58,8 @@ def display_my_recipe_container(my_recipe_list): for i, my_recipe in enumerate(my_recipe_list[:5]): with cols[i]: with st.container(border=True): - st.image(my_recipe["recipe_img_url"]) + # st.image(my_recipe["recipe_img_url"]) + st.markdown(f'Your Image', unsafe_allow_html=True) st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) def display_recommended_container(recommended_list): @@ -78,7 +79,8 @@ def display_recommended_container(recommended_list): for i, recommended in enumerate(recommended_list[:5]): with cols[i]: with st.container(border=True): - st.image(recommended["recipe_img_url"]) + # st.image(recommended["recipe_img_url"]) + st.markdown(f'Your Image', unsafe_allow_html=True) st.markdown(f"

{recommended['recipe_name']}

", unsafe_allow_html=True) def main_page_2(): From 2e4f433381b6f7993eca3ea72327e0d096b3b0f3 Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Mon, 25 Mar 2024 01:45:31 +0900 Subject: [PATCH 143/187] =?UTF-8?q?feat:=20CB=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B8=94=EB=9E=9C=EB=94=A9=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20#62?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airflow/dags/batch_inference.py | 118 ++++++++++++++++++++++++---- airflow/dags/db_operations.py | 124 +++++++++++++++++++++++++----- airflow/dags/recbole_inference.py | 1 - 3 files changed, 209 insertions(+), 34 deletions(-) diff --git a/airflow/dags/batch_inference.py b/airflow/dags/batch_inference.py index 7969755..72ea626 100644 --- a/airflow/dags/batch_inference.py +++ b/airflow/dags/batch_inference.py @@ -9,15 +9,19 @@ from airflow.operators.python import PythonOperator from db_config import db_host, db_port -from db_operations import fetch_user_history, update_model_recommendations +from db_operations import fetch_user_history, update_model_recommendations, cb_inference, blending_results from recbole_inference import sasrec_inference -def fetch_and_push_user_history(**context): - user_id_and_feedbacks = fetch_user_history() - context["ti"].xcom_push(key='user_id_and_feedbacks', value=user_id_and_feedbacks) +def fetch_and_push_user_history(result_type:str=None, **context): + if result_type: + user_id_and_feedbacks = fetch_user_history(result_type) + context["ti"].xcom_push(key='user_id_and_feedbacks_cb', value=user_id_and_feedbacks) + else: + user_id_and_feedbacks = fetch_user_history() + context["ti"].xcom_push(key='user_id_and_feedbacks_hybrid', value=user_id_and_feedbacks) -def batch_inference(**context): - user_id_and_feedbacks = context["ti"].xcom_pull(key='user_id_and_feedbacks') +def hybrid_inference(**context): + user_id_and_feedbacks = context["ti"].xcom_pull(key='user_id_and_feedbacks_hybrid') # 설정 파일과 모델 저장 경로 설정 modelpath = '/home/judy/level2-3-recsys-finalproject-recsys-01/ml/Sequential/saved/BERT4Rec-Mar-24-2024_00-51-09.pth' @@ -26,12 +30,36 @@ def batch_inference(**context): modelpath, user_id_and_feedbacks) - context["ti"].xcom_push(key='recommended_results', value=recommended_results) + context["ti"].xcom_push(key='hybrid_recommended_results', value=recommended_results) -def save_results(collection_name, **context): - recommended_results = context["ti"].xcom_pull(key='recommended_results') +def save_results_hybrid(collection_name, **context): + recommended_results = context["ti"].xcom_pull(key='hybrid_recommended_results') update_model_recommendations(recommended_results, collection_name) +def cb_inference_(**context): + user_id_and_feedbacks = context["ti"].xcom_pull(key='user_id_and_feedbacks_cb') + + recommended_results = cb_inference( + user_id_and_feedbacks) + + context["ti"].xcom_push(key='cb_recommended_results', value=recommended_results) + +def save_results_cb(collection_name, input_type, **context): + recommended_results = context["ti"].xcom_pull(key='cb_recommended_results') + update_model_recommendations(recommended_results, collection_name, input_type=input_type) + +def blending_results_(**context): + hybrid_recommended_results = context["ti"].xcom_pull(key='hybrid_recommended_results') + cb_recommended_results = context["ti"].xcom_pull(key='cb_recommended_results') + + blended_results = blending_results(hybrid_recommended_results, cb_recommended_results) + + context["ti"].xcom_push(key='blended_recommended_results', value=blended_results) + +def save_results_blended(collection_name, input_type, **context): + recommended_results = context["ti"].xcom_pull(key='blended_recommended_results') + update_model_recommendations(recommended_results, collection_name, input_type=input_type) + with DAG( dag_id="batch_inference", description="batch inference of all active users using MultiDAE", @@ -42,7 +70,7 @@ def save_results(collection_name, **context): # get active user t1 = PythonOperator( - task_id="fetch_and_push_user_history", + task_id="fetch_and_push_user_history_for_hybrid", python_callable=fetch_and_push_user_history, depends_on_past=False, owner="judy", @@ -53,17 +81,17 @@ def save_results(collection_name, **context): # hybrid inference t2 = PythonOperator( task_id="batch_inference_by_hybrid", - python_callable=batch_inference, + python_callable=hybrid_inference, depends_on_past=False, owner="judy", retries=3, retry_delay=timedelta(minutes=5), ) - # save results + # save results1 t3 = PythonOperator( - task_id="save_results", - python_callable=save_results, + task_id="save_results_hybrid", + python_callable=save_results_hybrid, depends_on_past=False, owner="judy", retries=3, @@ -73,4 +101,66 @@ def save_results(collection_name, **context): retry_delay=timedelta(minutes=5), ) + # get active user + t4 = PythonOperator( + task_id="fetch_and_push_user_history_for_cb", + python_callable=fetch_and_push_user_history, + depends_on_past=False, + owner="judy", + retries=3, + op_kwargs={ + 'result_type': 'recipe_id', + }, + retry_delay=timedelta(minutes=5), + ) + + # content-based inference + t5 = PythonOperator( + task_id="batch_inference_by_content_based", + python_callable=cb_inference_, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # save results2 + t6 = PythonOperator( + task_id="save_results_cb", + python_callable=save_results_cb, + depends_on_past=False, + owner="judy", + retries=3, + op_kwargs={ + 'collection_name': 'model_recommendation_history_cb', + 'input_type': 'recipe_id', + }, + retry_delay=timedelta(minutes=5), + ) + + # content-based inference + t7 = PythonOperator( + task_id="blending_results", + python_callable=blending_results_, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # save results3 + t8 = PythonOperator( + task_id="save_results_blended", + python_callable=save_results_blended, + depends_on_past=False, + owner="judy", + retries=3, + op_kwargs={ + 'collection_name': 'model_recommendation_history_total', + 'input_type': 'recipe_id', + }, + retry_delay=timedelta(minutes=5), + ) t1 >> t2 >> t3 + t4 >> t5 >> t6 + [t2, t5] >> t7 >> t8 diff --git a/airflow/dags/db_operations.py b/airflow/dags/db_operations.py index 5c0c6f6..a8abde3 100644 --- a/airflow/dags/db_operations.py +++ b/airflow/dags/db_operations.py @@ -5,7 +5,7 @@ from db_config import db_host, db_port -def fetch_user_history(): +def fetch_user_history(result_type='recipe_sno'): # db setting client = MongoClient(host=db_host, port=db_port) @@ -25,38 +25,124 @@ def fetch_user_history(): for r in db['recipes'].find({'_id': recipe}): recipe_snos.append(r['recipe_sno']) - user_id_and_feedbacks.append({ - '_id': str(u['_id']), - 'feedbacks': recipe_snos, - }) + if result_type == 'recipe_sno': + user_id_and_feedbacks.append({ + '_id': str(u['_id']), + 'feedbacks': recipe_snos, + }) + else: + user_id_and_feedbacks.append({ + '_id': str(u['_id']), + 'feedbacks': [str(feedback) for feedback in feedbacks], + }) return user_id_and_feedbacks -def update_model_recommendations(recommended_results, collection_name, meta={}): +def update_model_recommendations(recommended_results, collection_name, meta={}, input_type='recipe_sno'): # db setting client = MongoClient(host=db_host, port=db_port) db = client.dev - #print(recommended_results) - for recommended_result in recommended_results: - # print(recommended_result['_id']) + if input_type == 'recipe_sno': + for recommended_result in recommended_results: + recommended_items = [] + for recipe in recommended_result['recommended_items']: + for r in db['recipes'].find({'recipe_sno': recipe}): + recommended_items.append(r['_id']) + break + recommended_result['recommended_items'] = recommended_items + + data = [{ + 'user_id': ObjectId(recommended_result['_id']), + 'recommended_recipes': recommended_result['recommended_items'], + 'datetime': dt.now(), + **meta + } for recommended_result in recommended_results] + else: + data = [{ + 'user_id': ObjectId(recommended_result['_id']), + 'recommended_recipes': [ObjectId(recipe_id) for recipe_id in recommended_result['recommended_items']], + 'datetime': dt.now(), + **meta + } for recommended_result in recommended_results] + + db[collection_name].insert_many(data) + + print('push data into db') + +def cb_inference(user_id_and_feedbacks: list, k: int=20, batch_size: int=4096): + + # db setting + client = MongoClient(host=db_host, port=db_port) + db = client.dev + + recommended_results = [] + for user_data in user_id_and_feedbacks: + user_recommended = [] + for feedback in user_data['feedbacks'][::-1][:10]: + for r in db['train_recipes'].find({'_id': ObjectId(feedback)}): + user_recommended.append(str(r['closest_recipe'])) + break + + recommended_results.append({ + '_id': user_data['_id'], + 'recommended_items': user_recommended + }) + + return recommended_results + +def blending_results(hybrid_results, cb_results): + + # db setting + client = MongoClient(host=db_host, port=db_port) + db = client.dev + + # hybrid tokens to recipe id + for recommended_result in hybrid_results: recommended_items = [] for recipe in recommended_result['recommended_items']: for r in db['recipes'].find({'recipe_sno': recipe}): - recommended_items.append(r['_id']) - # print(r['food_name'], end=' | ') + recommended_items.append(str(r['_id'])) break recommended_result['recommended_items'] = recommended_items - data = [{ - 'user_id': ObjectId(recommended_result['_id']), - 'recommended_recipes': recommended_result['recommended_items'], - 'datetime': dt.now(), - **meta - } for recommended_result in recommended_results] + blended_results = [] + for user_hybrid, user_cb in zip(hybrid_results, cb_results): - db[collection_name].insert_many(data) + user_id = user_hybrid['_id'] + blended_result = set(user_hybrid['recommended_items'][:10] + user_cb['recommended_items']) - print('push data into db') + i = 10 + while len(blended_result) < 20: + blended_result = blended_result | {user_hybrid['recommended_items'][i]} + i += 1 + + blended_results.append({ + '_id': user_id, + 'recommended_items': list(blended_result) + }) + + return blended_results + +if __name__ == '__main__': + + # 데이터 얻기 + user_id_and_feedbacks = fetch_user_history(result_type='recipe_id') + + recommended_items = cb_inference( + user_id_and_feedbacks) + + print(recommended_items) + + # 추천 결과 확인하기 + client = MongoClient(host=db_host, port=db_port) + db = client.dev + + for recommended_item in recommended_items: + print(recommended_item['_id']) + for recipe in recommended_item['recommended_items']: + for r in db['recipes'].find({'_id': recipe}): + print(r['food_name'], end=' | ') + print() diff --git a/airflow/dags/recbole_inference.py b/airflow/dags/recbole_inference.py index 2867667..8787160 100644 --- a/airflow/dags/recbole_inference.py +++ b/airflow/dags/recbole_inference.py @@ -62,7 +62,6 @@ def sasrec_inference(modelpath: str, user_id_and_feedbacks: list, k: int=20, bat return recommended_result - if __name__ == '__main__': # 설정 파일과 모델 저장 경로 설정 From 55a0501b8fc95e5c1640d1afa619f97db462e145 Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Mon, 25 Mar 2024 02:37:58 +0900 Subject: [PATCH 144/187] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=84=9C=EB=B9=99=20dag=20=EA=B5=AC=ED=98=84=20#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- airflow/dags/batch_inference.py | 22 ++--- airflow/dags/db_operations.py | 35 ++++++- airflow/dags/realtime_serving.py | 161 +++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 airflow/dags/realtime_serving.py diff --git a/airflow/dags/batch_inference.py b/airflow/dags/batch_inference.py index 72ea626..19f7373 100644 --- a/airflow/dags/batch_inference.py +++ b/airflow/dags/batch_inference.py @@ -1,23 +1,19 @@ -from pymongo import MongoClient - from datetime import datetime as dt from datetime import timedelta from airflow import DAG from airflow.utils.dates import days_ago -from airflow.operators.bash import BashOperator from airflow.operators.python import PythonOperator -from db_config import db_host, db_port -from db_operations import fetch_user_history, update_model_recommendations, cb_inference, blending_results +from db_operations import fetch_user_histories, update_model_recommendations, cb_inference, blending_results from recbole_inference import sasrec_inference -def fetch_and_push_user_history(result_type:str=None, **context): +def fetch_and_push_user_histories(result_type:str=None, **context): if result_type: - user_id_and_feedbacks = fetch_user_history(result_type) + user_id_and_feedbacks = fetch_user_histores(result_type) context["ti"].xcom_push(key='user_id_and_feedbacks_cb', value=user_id_and_feedbacks) else: - user_id_and_feedbacks = fetch_user_history() + user_id_and_feedbacks = fetch_user_histories() context["ti"].xcom_push(key='user_id_and_feedbacks_hybrid', value=user_id_and_feedbacks) def hybrid_inference(**context): @@ -62,7 +58,7 @@ def save_results_blended(collection_name, input_type, **context): with DAG( dag_id="batch_inference", - description="batch inference of all active users using MultiDAE", + description="batch inference of all active users using BERT4Rec", start_date=days_ago(5), # DAG 정의 기준 2일 전부터 시작합니다. schedule_interval="0 2 * * *", # 매일 2시에 시작 tags=["basket_recommendation", "inference"], @@ -70,8 +66,8 @@ def save_results_blended(collection_name, input_type, **context): # get active user t1 = PythonOperator( - task_id="fetch_and_push_user_history_for_hybrid", - python_callable=fetch_and_push_user_history, + task_id="fetch_and_push_user_histories_for_hybrid", + python_callable=fetch_and_push_user_histories, depends_on_past=False, owner="judy", retries=3, @@ -103,8 +99,8 @@ def save_results_blended(collection_name, input_type, **context): # get active user t4 = PythonOperator( - task_id="fetch_and_push_user_history_for_cb", - python_callable=fetch_and_push_user_history, + task_id="fetch_and_push_user_histories_for_cb", + python_callable=fetch_and_push_user_histories, depends_on_past=False, owner="judy", retries=3, diff --git a/airflow/dags/db_operations.py b/airflow/dags/db_operations.py index a8abde3..9234cb2 100644 --- a/airflow/dags/db_operations.py +++ b/airflow/dags/db_operations.py @@ -5,7 +5,40 @@ from db_config import db_host, db_port -def fetch_user_history(result_type='recipe_sno'): +def fetch_user_history(user_id, result_type='recipe_sno'): + + # db setting + client = MongoClient(host=db_host, port=db_port) + db = client.dev + + # user + user_id_and_feedbacks = [] + for u in db['users'].find({'_id':ObjectId(user_id)}): # 원랜 여기가 find() + # initial_feedback이 있음 + feedbacks = u['initial_feedback_history'] + # 추가 피드백 있는 경우 + if 'feedback_history' in u: + feedbacks.append(u['feedback_history']) + # 피드백 _id를 recipe_sno 로 변경 + recipe_snos = [] + for recipe in feedbacks: + for r in db['recipes'].find({'_id': recipe}): + recipe_snos.append(r['recipe_sno']) + + if result_type == 'recipe_sno': + user_id_and_feedbacks.append({ + '_id': str(u['_id']), + 'feedbacks': recipe_snos, + }) + else: + user_id_and_feedbacks.append({ + '_id': str(u['_id']), + 'feedbacks': [str(feedback) for feedback in feedbacks], + }) + + return user_id_and_feedbacks + +def fetch_user_histories(result_type='recipe_sno'): # db setting client = MongoClient(host=db_host, port=db_port) diff --git a/airflow/dags/realtime_serving.py b/airflow/dags/realtime_serving.py new file mode 100644 index 0000000..b68a2d0 --- /dev/null +++ b/airflow/dags/realtime_serving.py @@ -0,0 +1,161 @@ +from datetime import datetime as dt +from datetime import timedelta + +from airflow import DAG +from airflow.operators.python import PythonOperator + +from db_operations import fetch_user_history, update_model_recommendations, cb_inference, blending_results +from recbole_inference import sasrec_inference + +def fetch_and_push_user_history(result_type:str=None, **context): + conf = context.get('dag_run').conf + user_id = conf.get('user_id') + if result_type: + user_id_and_feedbacks = fetch_user_history(user_id, result_type) + context["ti"].xcom_push(key='user_id_and_feedbacks_cb', value=user_id_and_feedbacks) + else: + user_id_and_feedbacks = fetch_user_history(user_id) + context["ti"].xcom_push(key='user_id_and_feedbacks_hybrid', value=user_id_and_feedbacks) + +def hybrid_inference(**context): + user_id_and_feedbacks = context["ti"].xcom_pull(key='user_id_and_feedbacks_hybrid') + + # 설정 파일과 모델 저장 경로 설정 + modelpath = '/home/judy/level2-3-recsys-finalproject-recsys-01/ml/Sequential/saved/BERT4Rec-Mar-24-2024_00-51-09.pth' + + recommended_results = sasrec_inference( + modelpath, + user_id_and_feedbacks) + + context["ti"].xcom_push(key='hybrid_recommended_results', value=recommended_results) + +def save_results_hybrid(collection_name, **context): + recommended_results = context["ti"].xcom_pull(key='hybrid_recommended_results') + update_model_recommendations(recommended_results, collection_name) + +def cb_inference_(**context): + user_id_and_feedbacks = context["ti"].xcom_pull(key='user_id_and_feedbacks_cb') + + recommended_results = cb_inference( + user_id_and_feedbacks) + + context["ti"].xcom_push(key='cb_recommended_results', value=recommended_results) + +def save_results_cb(collection_name, input_type, **context): + recommended_results = context["ti"].xcom_pull(key='cb_recommended_results') + update_model_recommendations(recommended_results, collection_name, input_type=input_type) + +def blending_results_(**context): + hybrid_recommended_results = context["ti"].xcom_pull(key='hybrid_recommended_results') + cb_recommended_results = context["ti"].xcom_pull(key='cb_recommended_results') + + blended_results = blending_results(hybrid_recommended_results, cb_recommended_results) + + context["ti"].xcom_push(key='blended_recommended_results', value=blended_results) + +def save_results_blended(collection_name, input_type, **context): + recommended_results = context["ti"].xcom_pull(key='blended_recommended_results') + update_model_recommendations(recommended_results, collection_name, input_type=input_type) + +with DAG( + dag_id="realtime_serving", + description="realtime serving for new user", + tags=["basket_recommendation", "inference"], + ) as dag: + + # get active user + t1 = PythonOperator( + task_id="fetch_and_push_user_history_for_hybrid", + python_callable=fetch_and_push_user_history, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # hybrid inference + t2 = PythonOperator( + task_id="batch_inference_by_hybrid", + python_callable=hybrid_inference, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # save results1 + t3 = PythonOperator( + task_id="save_results_hybrid", + python_callable=save_results_hybrid, + depends_on_past=False, + owner="judy", + retries=3, + op_kwargs={ + 'collection_name': 'model_recommendation_history_hybrid', + 'meta': {'model_version': '0.0.1'}}, + retry_delay=timedelta(minutes=5), + ) + + # get active user + t4 = PythonOperator( + task_id="fetch_and_push_user_history_for_cb", + python_callable=fetch_and_push_user_history, + depends_on_past=False, + owner="judy", + retries=3, + op_kwargs={ + 'result_type': 'recipe_id', + }, + retry_delay=timedelta(minutes=5), + ) + + # content-based inference + t5 = PythonOperator( + task_id="batch_inference_by_content_based", + python_callable=cb_inference_, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # save results2 + t6 = PythonOperator( + task_id="save_results_cb", + python_callable=save_results_cb, + depends_on_past=False, + owner="judy", + retries=3, + op_kwargs={ + 'collection_name': 'model_recommendation_history_cb', + 'input_type': 'recipe_id', + }, + retry_delay=timedelta(minutes=5), + ) + + # content-based inference + t7 = PythonOperator( + task_id="blending_results", + python_callable=blending_results_, + depends_on_past=False, + owner="judy", + retries=3, + retry_delay=timedelta(minutes=5), + ) + + # save results3 + t8 = PythonOperator( + task_id="save_results_blended", + python_callable=save_results_blended, + depends_on_past=False, + owner="judy", + retries=3, + op_kwargs={ + 'collection_name': 'model_recommendation_history_total', + 'input_type': 'recipe_id', + }, + retry_delay=timedelta(minutes=5), + ) + t1 >> t2 >> t3 + t4 >> t5 >> t6 + [t2, t5] >> t7 >> t8 From 4063383b99d60c542ebf5b49b442c970099959a9 Mon Sep 17 00:00:00 2001 From: Juyeon Lee Date: Mon, 25 Mar 2024 02:54:34 +0900 Subject: [PATCH 145/187] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0+?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=ED=94=BC=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A6=AC=EB=B7=B0=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EB=A7=8C=20=ED=99=9C=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ml/eval/4.make-iter.ipynb | 176 +++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 78 deletions(-) diff --git a/ml/eval/4.make-iter.ipynb b/ml/eval/4.make-iter.ipynb index 36c1e20..310359e 100644 --- a/ml/eval/4.make-iter.ipynb +++ b/ml/eval/4.make-iter.ipynb @@ -39,7 +39,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "train_df shape: (276847, 4)\n" + "train_df shape: (244773, 4)\n" ] }, { @@ -71,51 +71,51 @@ " \n", " \n", " \n", - " 239004\n", - " 19831374\n", - " 6862680\n", - " 5.0\n", - " 2017-04-04 06:06\n", + " 190635\n", + " 60086420\n", + " 6890715\n", + " 5\n", + " 2021-05-09 14:01\n", " \n", " \n", - " 146787\n", - " 75997033\n", - " 6837870\n", - " 5.0\n", - " 2016-01-16 01:13\n", + " 28817\n", + " 80311425\n", + " 6856312\n", + " 4\n", + " 2016-10-19 21:56\n", " \n", " \n", - " 76025\n", - " 19572058\n", - " 6866483\n", - " 5.0\n", - " 2023-12-24 19:59\n", + " 60648\n", + " 32128977\n", + " 4243352\n", + " 5\n", + " 2020-07-01 05:59\n", " \n", " \n", - " 51968\n", - " 58491660\n", - " 6834238\n", - " 3.0\n", - " 2015-12-03 09:31\n", + " 213179\n", + " 61069740\n", + " 5420128\n", + " 5\n", + " 2020-12-28 20:44\n", " \n", " \n", - " 98090\n", - " 92219780\n", - " 6844127\n", - " 5.0\n", - " 2017-04-11 03:25\n", + " 74041\n", + " heart2008\n", + " 6899335\n", + " 5\n", + " 2020-06-30 16:59\n", " \n", " \n", "\n", "" ], "text/plain": [ - " uid recipe_sno rating datetime\n", - "239004 19831374 6862680 5.0 2017-04-04 06:06\n", - "146787 75997033 6837870 5.0 2016-01-16 01:13\n", - "76025 19572058 6866483 5.0 2023-12-24 19:59\n", - "51968 58491660 6834238 3.0 2015-12-03 09:31\n", - "98090 92219780 6844127 5.0 2017-04-11 03:25" + " uid recipe_sno rating datetime\n", + "190635 60086420 6890715 5 2021-05-09 14:01\n", + "28817 80311425 6856312 4 2016-10-19 21:56\n", + "60648 32128977 4243352 5 2020-07-01 05:59\n", + "213179 61069740 5420128 5 2020-12-28 20:44\n", + "74041 heart2008 6899335 5 2020-06-30 16:59" ] }, "execution_count": 3, @@ -124,7 +124,7 @@ } ], "source": [ - "df = pd.read_csv(os.path.join(data_dir, 'merged-data-over-5-240321.csv'))\n", + "df = pd.read_csv(os.path.join(data_dir, 'merged-data-over-5-240325.csv'))\n", "print('train_df shape: ', df.shape)\n", "df.sample(5)" ] @@ -133,6 +133,26 @@ "cell_type": "code", "execution_count": 4, "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(20281, 44588)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['uid'].nunique(), df.recipe_sno.nunique()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, "outputs": [ { "data": { @@ -163,39 +183,39 @@ " \n", " \n", " \n", - " 53691\n", - " 61440273\n", - " 6883990\n", - " 5.0\n", - " 2020-02-09 19:59\n", + " 237559\n", + " 51165612\n", + " 6858634\n", + " 3\n", + " 2016-10-25 11:48\n", " \n", " \n", - " 68528\n", - " 69367549\n", - " 6839837\n", - " 5.0\n", - " 2017-05-23 19:31\n", + " 119544\n", + " 86576172\n", + " 6868134\n", + " 5\n", + " 2017-06-16 22:31\n", " \n", " \n", - " 220028\n", - " 88015859\n", - " 6864234\n", - " 4.0\n", - " 2018-11-21 02:04\n", + " 140289\n", + " 18249756\n", + " 6912220\n", + " 5\n", + " 2020-06-14 14:53\n", " \n", " \n", - " 276400\n", - " 15333309\n", - " 6922749\n", - " 5.0\n", - " 2023-12-13 21:48\n", + " 215687\n", + " 73743440\n", + " 6833197\n", + " 5\n", + " 2019-04-10 21:09\n", " \n", " \n", - " 178640\n", - " 39765052\n", - " 6894679\n", - " 5.0\n", - " 2022-06-28 18:09\n", + " 242417\n", + " 12258766\n", + " 6867060\n", + " 5\n", + " 2017-04-11 13:28\n", " \n", " \n", "\n", @@ -203,14 +223,14 @@ ], "text/plain": [ " uid recipe_sno rating datetime\n", - "53691 61440273 6883990 5.0 2020-02-09 19:59\n", - "68528 69367549 6839837 5.0 2017-05-23 19:31\n", - "220028 88015859 6864234 4.0 2018-11-21 02:04\n", - "276400 15333309 6922749 5.0 2023-12-13 21:48\n", - "178640 39765052 6894679 5.0 2022-06-28 18:09" + "237559 51165612 6858634 3 2016-10-25 11:48\n", + "119544 86576172 6868134 5 2017-06-16 22:31\n", + "140289 18249756 6912220 5 2020-06-14 14:53\n", + "215687 73743440 6833197 5 2019-04-10 21:09\n", + "242417 12258766 6867060 5 2017-04-11 13:28" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -223,7 +243,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -232,7 +252,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -263,31 +283,31 @@ " \n", " \n", " \n", - " 240220\n", + " 208259\n", " 00700070\n", " 6856432\n", " 1.503859e+09\n", " \n", " \n", - " 240219\n", + " 208258\n", " 00700070\n", " 6885928\n", " 1.534357e+09\n", " \n", " \n", - " 240218\n", + " 208257\n", " 00700070\n", " 6886836\n", " 1.546958e+09\n", " \n", " \n", - " 240217\n", + " 208256\n", " 00700070\n", " 6892249\n", " 1.548445e+09\n", " \n", " \n", - " 240216\n", + " 208255\n", " 00700070\n", " 6849655\n", " 1.549552e+09\n", @@ -298,14 +318,14 @@ ], "text/plain": [ " user_id:token item_id:token timestamp:float\n", - "240220 00700070 6856432 1.503859e+09\n", - "240219 00700070 6885928 1.534357e+09\n", - "240218 00700070 6886836 1.546958e+09\n", - "240217 00700070 6892249 1.548445e+09\n", - "240216 00700070 6849655 1.549552e+09" + "208259 00700070 6856432 1.503859e+09\n", + "208258 00700070 6885928 1.534357e+09\n", + "208257 00700070 6886836 1.546958e+09\n", + "208256 00700070 6892249 1.548445e+09\n", + "208255 00700070 6849655 1.549552e+09" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -322,7 +342,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ From a1102ae727f289ccba5451320577acb2b0219504 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 25 Mar 2024 09:44:30 +0900 Subject: [PATCH 146/187] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EC=84=9C=EB=B9=99=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?#66?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../users/controller/user_controller.py | 35 +++++++++++++++++-- .../users/repository/user_repository.py | 12 ++++--- .../api/routes/users/service/user_service.py | 7 ++-- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index 11cd1e8..de24c6b 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -1,4 +1,7 @@ -from fastapi import APIRouter, Query, Response, status +import requests +import json +import os +from fastapi import APIRouter, Query, Response, status, HTTPException from typing import Optional from datetime import datetime as dt @@ -54,7 +57,35 @@ async def favor_recipes(self, page_num: int) -> list: } async def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: - return self.user_service.save_favor_recipes(login_id, request) + # 선호 레시피 목록 저장 + user_id = self.user_service.save_favor_recipes(login_id, request) + + host = 'http://10.0.7.8:8000' + dag_id = 'realtime_serving' + + username = os.getenv("AIRFLOW_USERNAME") + password = os.getenv("AIRFLOW_PASSWORD") + + if username is None or password is None: + raise ValueError("AIRFLOW 유저 정보가 누락되었습니다.") + + header = { + 'Content-type': 'application/json', + 'Accept': 'application/json', + } + + request_body = json.dumps({ + "conf": { "user_id": user_id }, + }) + + response = requests.post(url=f"{host}/api/v1/dags/{dag_id}/dagRuns", headers=header, data=request_body, auth=(username, password)) + + if response.status_code != 200: + # 오류 시 선호 레시피 목록 롤백 + request.recipes = [] + self.user_service.save_favor_recipes(login_id, request) + raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="잠시 후 다시 시도해주세요.") + async def recommended_basket(self, user_id: str, price: int): # top k recipes id 가져옴 diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index f03504f..41107b2 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -27,11 +27,13 @@ def find_one(self, query: dict) -> UserSignupDTO: logging.debug(result) return result - def update_food(self, login_id: str, foods: list) -> int: + def update_food(self, login_id: str, foods: list) -> str: query = {'login_id': login_id} update_value = {'$set': {'initial_feedback_history': foods}} - result = self.collection.update_one(query, update_value) - return result.modified_count + self.collection.update_one(query, update_value) + + user = self.collection.find_one(query) + return str(user['_id']) def update_recommended_basket(self, login_id: str, recipe_list: list): query = {'login_id': login_id} @@ -76,8 +78,8 @@ class RecommendationRepository: def __init__(self): self.collection = data_source.collection_with_name_as('model_recommendation_histories') - def find_by_login_id(self, login_id: str) -> list: - result = self.collection.find_one({'id': login_id}) + def find_by_user_id(self, user_id: str) -> list: + result = self.collection.find_one({'id': user_id}) return result['recommended_item'] class BasketRepository: diff --git a/backend/app/api/routes/users/service/user_service.py b/backend/app/api/routes/users/service/user_service.py index 91a15b9..36c4964 100644 --- a/backend/app/api/routes/users/service/user_service.py +++ b/backend/app/api/routes/users/service/user_service.py @@ -59,12 +59,13 @@ def is_nickname_usable(self, nickname: str) -> bool: def favor_recipes(self, page_num: int) -> list: return self.food_repository.find_foods(page_num) - def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> None: - self.user_repository.update_food(login_id, request.recipes) + def save_favor_recipes(self, login_id: str, request: UserFavorRecipesRequest) -> str: + return self.user_repository.update_food(login_id, request.recipes) def top_k_recipes(self, login_id: str, price: int) -> list: + user = self.user_repository.find_one({"login_id": login_id}) # user에 inference된 recipes - return self.recommendation_repository.find_by_login_id(login_id) + return self.recommendation_repository.find_by_user_id(user['_id']) def recommended_basket(self, recipe_infos: dict, price_infos: dict, price: int) -> dict: # 입력값 파싱 From 18832b9d6a8c89c528787a021c0c86407ad17721 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 25 Mar 2024 09:53:48 +0900 Subject: [PATCH 147/187] =?UTF-8?q?feat:=20=EC=9E=A5=EB=B0=94=EA=B5=AC?= =?UTF-8?q?=EB=8B=88=20=EC=B6=94=EC=B2=9C=EC=8B=9C=20=EC=84=9C=EB=B9=99=20?= =?UTF-8?q?=EB=8C=80=EC=83=81=20=EB=AF=B8=EC=A1=B4=EC=9E=AC=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=B6=94=EA=B0=80=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/routes/users/controller/user_controller.py | 3 +++ backend/app/api/routes/users/repository/user_repository.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/users/controller/user_controller.py b/backend/app/api/routes/users/controller/user_controller.py index de24c6b..3127d2d 100644 --- a/backend/app/api/routes/users/controller/user_controller.py +++ b/backend/app/api/routes/users/controller/user_controller.py @@ -91,6 +91,9 @@ async def recommended_basket(self, user_id: str, price: int): # top k recipes id 가져옴 top_k_recipes = self.user_service.top_k_recipes(user_id, price) + if top_k_recipes is None or len(top_k_recipes) <= 0: + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="처리가 지연되고 있습니다. 잠시 후 다시 시도해주세요.") + # recipe 정보 가져오기 recipe_infos = self.recipe_service.get_recipes_by_recipes_id(top_k_recipes) diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index 41107b2..a3415c2 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -80,7 +80,7 @@ def __init__(self): def find_by_user_id(self, user_id: str) -> list: result = self.collection.find_one({'id': user_id}) - return result['recommended_item'] + return result['recommended_item'] if result is not None else [] class BasketRepository: def __init__(self): From 4c0e3fd7b8b814a6ccfb7e12e554c75f165b1949 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 25 Mar 2024 10:31:44 +0900 Subject: [PATCH 148/187] =?UTF-8?q?feat:=20top=20k=20=EB=A0=88=EC=8B=9C?= =?UTF-8?q?=ED=94=BC=20=EA=B0=80=EC=9E=A5=20=EC=B5=9C=EC=8B=A0=20=EB=8C=80?= =?UTF-8?q?=EC=83=81=20=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/routes/users/repository/user_repository.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index a3415c2..892d7f0 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -24,7 +24,7 @@ def all_users(self) -> list[UserSignupDTO]: def find_one(self, query: dict) -> UserSignupDTO: result = self.collection.find_one(query) - logging.debug(result) + # logging.debug(result) return result def update_food(self, login_id: str, foods: list) -> str: @@ -64,7 +64,7 @@ def find_foods(self, page_num: int, page_size: int=16) -> list: # logging.debug(results) results = list(results) total_size = len(results) - logging.debug('total_size', total_size) + # logging.debug('total_size', total_size) lst = [] for i, result in enumerate(results): if i == page_size: break @@ -79,8 +79,9 @@ def __init__(self): self.collection = data_source.collection_with_name_as('model_recommendation_histories') def find_by_user_id(self, user_id: str) -> list: - result = self.collection.find_one({'id': user_id}) - return result['recommended_item'] if result is not None else [] + result = self.collection.find({'id': user_id}).sort({'datetime':-1}).limit(1) + result = next(result, None) + return result['recommended_item'] if (result and 'recommended_item' in result) else [] class BasketRepository: def __init__(self): From f0f885fd37c1f5f04b98f3550c79f39508de5e8a Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Mon, 25 Mar 2024 11:29:13 +0900 Subject: [PATCH 149/187] Update price crawling --- .gitignore | 3 +++ crawling/crawl_price_db.py | 40 +++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 330f0e6..82d2e23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,linux # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,linux +### log +log/ + ### Linux ### *~ diff --git a/crawling/crawl_price_db.py b/crawling/crawl_price_db.py index 4145ded..6b69fda 100644 --- a/crawling/crawl_price_db.py +++ b/crawling/crawl_price_db.py @@ -1,7 +1,6 @@ import pandas as pd import pymongo from pymongo import MongoClient -import pprint import argparse from datetime import datetime import pandas as pd @@ -9,8 +8,6 @@ from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By -import json -import time import os import re from tqdm import tqdm @@ -92,12 +89,10 @@ def crawl_price(self): return min_price_document def main(args): - # MongoDB 연결 설정 - # client = MongoClient('mongodb://localhost:27017/') - + print('>>>> Test ?: ', args.test) create_upper_folder(args.log_path) - client = MongoClient(args.mongo_client) + client = MongoClient(args.mongo_client) # MongoDB 연결 설정 db = client['dev'] # 데이터베이스 선택 collection = db['ingredients'] # 컬렉션 선택 new_collection = db['prices'] @@ -110,10 +105,32 @@ def main(args): options.add_argument('--disable-dev-shm-usage') driver = webdriver.Chrome(service=service, options=options) - cursor = collection.find() + if args.test: + # crawled doc 만들기 + for document in tqdm(cursor): + if document['name'] == '': + crawled_document = {'_id' : document['_id'], + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None} + else: + try: + crawler = PriceCrawler(id = document['_id'], query=document['name']) + crawler.launch_crawler(driver) + crawled_document = crawler.crawl_price() + except Exception as e: + print(e) + crawled_document = {'_id' : document['_id'], + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None} + pass + for document in tqdm(cursor): # crawled doc 만들기 @@ -148,7 +165,6 @@ def main(args): log_exception(args.log_path, str(document['_id'])) pass except Exception as e: - # breakpoint() log_exception(args.log_path, str(document['_id'])) pass @@ -156,9 +172,11 @@ def main(args): if __name__ == '__main__': parser = argparse.ArgumentParser(description="parser") arg = parser.add_argument - arg("--mongo_client", type=str, default="mongodb://10.0.7.6:27017/") arg("--log_path", type=str, default = "log/price_db_error.txt") + arg("--test", type=bool, default = False) args = parser.parse_args() - main(args) \ No newline at end of file + main(args) + + \ No newline at end of file From bcc7e6ab29df17e7056dd11addafad51842b748c Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 25 Mar 2024 11:29:27 +0900 Subject: [PATCH 150/187] =?UTF-8?q?fix:=20=EA=B0=80=EA=B2=A9=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20merge=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#?= =?UTF-8?q?67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/api/routes/users/repository/user_repository.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/app/api/routes/users/repository/user_repository.py b/backend/app/api/routes/users/repository/user_repository.py index 892d7f0..522f1b1 100644 --- a/backend/app/api/routes/users/repository/user_repository.py +++ b/backend/app/api/routes/users/repository/user_repository.py @@ -1,4 +1,6 @@ from datetime import datetime + +from bson import ObjectId from app.database.data_source import data_source from ..dto.user_dto import UserSignupDTO, UserLoginDTO import logging @@ -76,12 +78,13 @@ def find_foods(self, page_num: int, page_size: int=16) -> list: class RecommendationRepository: def __init__(self): - self.collection = data_source.collection_with_name_as('model_recommendation_histories') + self.collection = data_source.collection_with_name_as('model_recommendation_history_total') def find_by_user_id(self, user_id: str) -> list: - result = self.collection.find({'id': user_id}).sort({'datetime':-1}).limit(1) + # logging.debug(user_id) + result = self.collection.find({'user_id': ObjectId(user_id)}).sort({'datetime':-1}).limit(1) result = next(result, None) - return result['recommended_item'] if (result and 'recommended_item' in result) else [] + return list(map(str, result['recommended_recipes'] if (result and 'recommended_recipes' in result) else [])) class BasketRepository: def __init__(self): From ac1e4588af3121cd39779b1baefa9570b5057568 Mon Sep 17 00:00:00 2001 From: GangBean Date: Mon, 25 Mar 2024 11:58:59 +0900 Subject: [PATCH 151/187] =?UTF-8?q?fix:=20=EC=B6=94=EC=B2=9C=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20#67?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recipes/repository/recipes_repository.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/app/api/routes/recipes/repository/recipes_repository.py b/backend/app/api/routes/recipes/repository/recipes_repository.py index bdb8e39..9a9bcb4 100644 --- a/backend/app/api/routes/recipes/repository/recipes_repository.py +++ b/backend/app/api/routes/recipes/repository/recipes_repository.py @@ -40,14 +40,15 @@ def select_ingredients_by_ingredients_id(self, ingredients_id: List[str]) -> Ing .find({"_id": { "$in": list(map(ObjectId, ingredients_id))} }) .sort({'ingredient_id': 1})) ingredient_ids = [amount['ingredient_id'] for amount in amounts] - ingredients = list(self.ingredients_collection - .find({"_id": {"$in": list(map(ObjectId, ingredient_ids))}}) - .sort({'_id': 1})) + # ingredients = list(self.ingredients_collection + # .find({"_id": {"$in": list(map(ObjectId, ingredient_ids))}}) + # .sort({'_id': 1})) + ingredients = [self.ingredients_collection.find_one({'_id': id}) for id in ingredient_ids] prices = list(next(self.prices_collection .find({"ingredient_id": ingredient_id}).sort({"date": -1}).limit(1)) for ingredient_id in ingredient_ids) # prices = list(self.prices_collection.find({"ingredient_id": {"$in": list(map(ObjectId, ingredient_ids))}})) - # logging.debug(len(amounts), len(ingredients), len(prices)) + logging.debug('[LEN]', len(amounts), len(ingredients), len(prices)) ingredient_list = list() for amount, ingredient, price in zip(amounts, ingredients, prices): # id: PyObjectId = Field(alias='_id', default=None) @@ -56,13 +57,13 @@ def select_ingredients_by_ingredients_id(self, ingredients_id: List[str]) -> Ing # price_url: str # amount: dict amount['name'] = ingredient['name'] - amount['price'] = price['price'] - amount['price_url'] = price['price_url'] + amount['price'] = price['price'] if price['price'] else 0 + amount['price_url'] = price['price_url'] if price['price_url'] else '' amount['amount'] = { 'value': amount['value'], 'unit': amount['unit'], } - amount['img_url'] = price['img_url'] + amount['img_url'] = price['img_url'] if price['img_url'] else '' ingredient_list.append(amount) # logging.debug('[RECIPE_REPOSITORY_RESULT]', ingredient_list) From bca4e67b4014f8564379475f298db5db61315d01 Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Mon, 25 Mar 2024 12:08:23 +0900 Subject: [PATCH 152/187] Fix _id to ingredient_id --- crawling/crawl_price_db.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crawling/crawl_price_db.py b/crawling/crawl_price_db.py index 2150eb3..e1a068e 100644 --- a/crawling/crawl_price_db.py +++ b/crawling/crawl_price_db.py @@ -51,7 +51,7 @@ def crawl_price(self): else: # 검색 결과 0개인 경우 - min_price_document = {'_id' : self.id, + min_price_document = {'ingredient_id' : self.id, 'product_name': None, 'date': None, 'price_url' : None, @@ -74,7 +74,7 @@ def crawl_price(self): price_num = price2num(a_tag.find_element(By.CLASS_NAME, 'price').text) product_name = a_tag.find_element(By.CLASS_NAME, 'title').text - new_document = {'_id' : self.id, + new_document = {'ingredient_id' : self.id, 'product_name': product_name, 'date': iso_format_time(datetime.now()), 'price_url' : item_url, @@ -114,7 +114,7 @@ def main(args): for document in tqdm(cursor): # crawled doc 만들기 if document['name'] == '': - crawled_document = {'_id' : document['_id'], + crawled_document = {'ingredient_id' : document['_id'], 'product_name': None, 'date': None, 'price_url' : None, @@ -129,7 +129,7 @@ def main(args): log_exception(args.log_path, str(document['_id'])) crawled_document = { - '_id' : document['_id'], + 'ingredient_id' : document['_id'], 'product_name': None, 'date': None, 'price_url' : None, @@ -143,7 +143,7 @@ def main(args): new_collection.insert_one(crawled_document) except pymongo.errors.DuplicateKeyError: try: - new_collection.update_one({'_id': document['_id']}, {"$set": crawled_document}, upsert=True) + new_collection.update_one({'ingredient_id': document['_id']}, {"$set": crawled_document}, upsert=True) except: log_exception(args.log_path, str(document['_id'])) pass From 23509650b20f79e5d771c045e578c7dd4b1f3890 Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Mon, 25 Mar 2024 12:08:53 +0900 Subject: [PATCH 153/187] Fix _id to ingredient_id --- crawling/crawl_price_db.py | 85 ++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 50 deletions(-) diff --git a/crawling/crawl_price_db.py b/crawling/crawl_price_db.py index 6b69fda..e1a068e 100644 --- a/crawling/crawl_price_db.py +++ b/crawling/crawl_price_db.py @@ -1,17 +1,13 @@ +import os, re, argparse +from datetime import datetime +from tqdm import tqdm import pandas as pd import pymongo from pymongo import MongoClient -import argparse -from datetime import datetime -import pandas as pd from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By -import os -import re -from tqdm import tqdm - def log_exception(fname, log): with open(fname, 'a+') as log_file: @@ -54,11 +50,13 @@ def crawl_price(self): divs = elem.find_elements(By.XPATH, "./div") else: # 검색 결과 0개인 경우 - min_price_document = {'_id' : self.id, + + min_price_document = {'ingredient_id' : self.id, 'product_name': None, 'date': None, 'price_url' : None, - 'img_url' : None} + 'img_url' : None, + 'price': None} return min_price_document @@ -75,12 +73,14 @@ def crawl_price(self): price_num = price2num(a_tag.find_element(By.CLASS_NAME, 'price').text) product_name = a_tag.find_element(By.CLASS_NAME, 'title').text - - new_document = {'_id' : self.id, + + new_document = {'ingredient_id' : self.id, 'product_name': product_name, 'date': iso_format_time(datetime.now()), 'price_url' : item_url, - 'img_url' : img_url} + 'img_url' : img_url, + 'price' : price_num, + } if price_num < min_price: min_price = price_num @@ -89,57 +89,37 @@ def crawl_price(self): return min_price_document def main(args): + print('>>>> Test ?: ', args.test) create_upper_folder(args.log_path) - client = MongoClient(args.mongo_client) # MongoDB 연결 설정 db = client['dev'] # 데이터베이스 선택 collection = db['ingredients'] # 컬렉션 선택 new_collection = db['prices'] service = ChromeService(ChromeDriverManager().install()) + user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36' options = webdriver.ChromeOptions() options.add_argument('--headless') options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') + options.add_argument(f'--user-agent={user_agent}') driver = webdriver.Chrome(service=service, options=options) - cursor = collection.find() + skip_count = args.skip_count + + cursor = collection.find().sort({'name':1}).skip(skip_count).limit(5000) - if args.test: - # crawled doc 만들기 - for document in tqdm(cursor): - if document['name'] == '': - crawled_document = {'_id' : document['_id'], - 'product_name': None, - 'date': None, - 'price_url' : None, - 'img_url' : None} - else: - try: - crawler = PriceCrawler(id = document['_id'], query=document['name']) - crawler.launch_crawler(driver) - crawled_document = crawler.crawl_price() - except Exception as e: - print(e) - crawled_document = {'_id' : document['_id'], - 'product_name': None, - 'date': None, - 'price_url' : None, - 'img_url' : None} - pass - - for document in tqdm(cursor): - # crawled doc 만들기 if document['name'] == '': - crawled_document = {'_id' : document['_id'], + crawled_document = {'ingredient_id' : document['_id'], 'product_name': None, 'date': None, 'price_url' : None, - 'img_url' : None} + 'img_url' : None, + 'price' : None} else: try: crawler = PriceCrawler(id = document['_id'], query=document['name']) @@ -147,12 +127,15 @@ def main(args): crawled_document = crawler.crawl_price() except Exception as e: log_exception(args.log_path, str(document['_id'])) - - crawled_document = {'_id' : document['_id'], - 'product_name': None, - 'date': None, - 'price_url' : None, - 'img_url' : None} + + crawled_document = { + 'ingredient_id' : document['_id'], + 'product_name': None, + 'date': None, + 'price_url' : None, + 'img_url' : None, + 'price': None + } pass # insert 하기 @@ -160,10 +143,12 @@ def main(args): new_collection.insert_one(crawled_document) except pymongo.errors.DuplicateKeyError: try: - new_collection.update_one({'_id': document['_id']}, {"$set": crawled_document}, upsert=True) + new_collection.update_one({'ingredient_id': document['_id']}, {"$set": crawled_document}, upsert=True) except: log_exception(args.log_path, str(document['_id'])) pass + except KeyboardInterrupt: + break except Exception as e: log_exception(args.log_path, str(document['_id'])) pass @@ -172,11 +157,11 @@ def main(args): if __name__ == '__main__': parser = argparse.ArgumentParser(description="parser") arg = parser.add_argument + arg("--mongo_client", type=str, default="mongodb://10.0.7.6:27017/") arg("--log_path", type=str, default = "log/price_db_error.txt") + arg("--skip_count", type=int, default=0) arg("--test", type=bool, default = False) args = parser.parse_args() main(args) - - \ No newline at end of file From bdbd1b3c90d8f130e48c5cda283a76c389d624ee Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Mon, 25 Mar 2024 12:11:13 +0900 Subject: [PATCH 154/187] Update import --- crawling/crawl_price_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crawling/crawl_price_db.py b/crawling/crawl_price_db.py index e1a068e..80de8d1 100644 --- a/crawling/crawl_price_db.py +++ b/crawling/crawl_price_db.py @@ -164,4 +164,4 @@ def main(args): arg("--test", type=bool, default = False) args = parser.parse_args() - main(args) + main(args) \ No newline at end of file From 5803948bd778c4e6a26f1b5823deeee159a69e00 Mon Sep 17 00:00:00 2001 From: Hyunjoo Lee Date: Tue, 26 Mar 2024 19:04:37 +0900 Subject: [PATCH 155/187] =?UTF-8?q?fix:=20streamlit=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=20=EC=A4=91=20=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app.py | 557 ----------------------- frontend/basket_login.py | 2 +- frontend/basket_signup.py | 2 - frontend/common.py | 91 +++- frontend/main.py | 6 +- frontend/main_2.py | 20 +- frontend/newapp.py | 43 -- frontend/pages/recommendation.py | 33 -- frontend/pages/recommendation_history.py | 12 - frontend/pages/user_history.py | 12 - frontend/recommendation.py | 7 +- frontend/recommendation_history.py | 8 +- frontend/result_page.py | 32 +- frontend/user_history.py | 33 +- 14 files changed, 126 insertions(+), 732 deletions(-) delete mode 100644 frontend/app.py delete mode 100644 frontend/newapp.py delete mode 100644 frontend/pages/recommendation.py delete mode 100644 frontend/pages/recommendation_history.py delete mode 100644 frontend/pages/user_history.py diff --git a/frontend/app.py b/frontend/app.py deleted file mode 100644 index b4b7950..0000000 --- a/frontend/app.py +++ /dev/null @@ -1,557 +0,0 @@ -import streamlit as st -import streamlit_antd_components as sac -from streamlit_extras.stylable_container import stylable_container -from st_supabase_connection import SupabaseConnection -from st_login_form import login_form -from streamlit_login_auth_ui.widgets import __login__ -import time, math - -import streamlit as st -import pandas as pd -import numpy as np - -from PIL import Image -import requests - -global api_prefix -api_prefix = "https://3cc9be7f-84ef-480e-af0d-f4e81b375f2e.mock.pstmn.io/api/" - -def display_ingredients_in_rows_of_four2(ingredients): - for ingredient in ingredients: - sub_container = st.container(border=True) - - with sub_container: - - cols = st.columns(5) - - with cols[0]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - with cols[1]: - st.write(ingredient['ingredient_name']) - st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) - - with cols[-1]: - st.link_button('구매', ingredient['market_url'], type='primary') - -def show_feedback_button(recipe_id, user_feedback): - - icon_mapper = lambda cooked: '❤️' if cooked else '🩶' - cooked = recipe_id in user_feedback - - st.button( - icon_mapper(cooked), - on_click=patch_feedback, - key=f'{recipe_id}_feedback_button', - args=(st.session_state.user, recipe_id, cooked)) - -def display_recipes_in_rows_of_four(recipe_list, user_feedback=None): - - for row in range(math.ceil(len(recipe_list)/4)): - cols = st.columns(4) - - for i in range(4): - item_idx = i + row * 4 - if item_idx >= len(recipe_list): break - - item = recipe_list[item_idx] - with cols[i]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - if user_feedback is None: - st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) - else: - sub_cols = st.columns([3,1]) - with sub_cols[0]: - st.markdown(f'

{item["recipe_name"]}

', unsafe_allow_html=True) - with sub_cols[-1]: - show_feedback_button(item['recipe_id'], user_feedback) - - -def get_and_stack_recipe_data_w_feedback(): - - url = api_prefix + "users/{user_id}/recipes/recommended?page={page_num}" - recipe_list, user_feedback = [], [] - formatted_url = url.format(user_id=st.session_state.user, page_num=1) - - while formatted_url: - print(formatted_url) - data = get_response(formatted_url) - recipe_list.extend(data['recipe_list']) - formatted_url = data['next_page_url'] - print(data['user_feedback']) - user_feedback = data['user_feedback'] - - return recipe_list, user_feedback - -def recommendation_history_page(): - - # 앱 헤더 - page_header() - - # get data - recipe_list, user_feedback = get_and_stack_recipe_data() - - # 페이지 구성 - container = st.container(border=True) - - with container: - - st.markdown("

AI 가 선정한 취향 저격 레시피

", unsafe_allow_html=True) - - sub_container = st.container(border=False) - with sub_container: - st.markdown("
❤️: 요리해봤어요
", unsafe_allow_html=True) - st.markdown("
🩶: 아직 안해봤어요
", unsafe_allow_html=True) - - display_ingredients_in_rows_of_four2(recipe_list, user_feedback) - - -def recommendation_page(): - - # 앱 헤더 - page_header() - - # 페이지 구성 - container = st.container(border=True) - - with container: - st.markdown("

이번 주 장바구니 만들기

", unsafe_allow_html=True) - st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) - st.markdown("
예산을 정해주세요.
", unsafe_allow_html=True) - - cols = st.columns([1,5,1]) - - with cols[1]: - - price = st.slider( - label='', min_value=10000, max_value=1000000, value=50000, step=5000 - ) - - cols = st.columns(5) - - with cols[2]: - st.write("예산: ", price, '원') - - - cols = st.columns(3) - - with cols[1]: - button2 = st.button("장바구니 추천받기", type="primary") - if button2: - st.session_state['page_info'] = 'result_page_1' - - - -def display_ingredients_in_rows_of_four(ingredients): - for ingredient in ingredients: - sub_container = st.container(border=True) - - with sub_container: - - cols = st.columns(5) - - with cols[0]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - with cols[1]: - st.write(ingredient['ingredient_name']) - st.write(ingredient['ingredient_amount'], ingredient['ingredient_unit']) - - with cols[-1]: - st.link_button('구매', ingredient['market_url'], type='primary') -def display_recipes_in_rows_of_four(recipes): - for recipe in recipes: - sub_container = st.container(border=True) - - with sub_container: - - cols = st.columns(2) - - with cols[0]: - st.markdown(f'Your Image', unsafe_allow_html=True) - - with cols[1]: - st.write(recipe['recipe_name']) - - - -def result_page_2(): - - # 앱 헤더 - page_header() - - url = api_prefix + "users/{user_id}/previousrecommendation" - formatted_url = url.format(user_id=st.session_state.user) - data = get_response(formatted_url) - - # 페이지 구성 - container = st.container(border=True) - - with container: - - # 장바구니 추천 문구 - st.markdown("

새로운 장바구니를 추천받았어요!

", unsafe_allow_html=True) - st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) - - st.divider() - - # 구매할 식료품 목록 - st.markdown("

추천 장바구니

", unsafe_allow_html=True) - - display_ingredients_in_rows_of_four(data['ingredient_list']) - total_price = sum([ingredient['ingredient_price'] for ingredient in data['ingredient_list']]) - - st.markdown(f"
예상 총 금액: {total_price} 원
", unsafe_allow_html=True) - - st.divider() - - # 이 장바구니로 만들 수 있는 음식 레시피 - st.markdown("

이 장바구니로 만들 수 있는 음식 레시피

", unsafe_allow_html=True) - display_recipes_in_rows_of_four(data['recipe_list']) - - st.text("\n\n") - basket_feedback() - - -def get_and_stack_recipe_data(): - - url = api_prefix + "users/{user_id}/recipes/cooked?page={page_num}" - recipe_list = [] - formatted_url = url.format(user_id=st.session_state.user, page_num=1) - - while formatted_url: - data = get_response(formatted_url) - recipe_list.extend(data['recipe_list']) - formatted_url = data['next_page_url'] - - return recipe_list - - - -def get_my_recipe_data(): - - url = api_prefix + "users/{user_id}/recipes/recommended?page={page_num}" - recipe_list = [] - formatted_url = url.format(user_id=st.session_state.user, page_num=1) - - while formatted_url: - data = get_response(formatted_url) - recipe_list.extend(data['recipe_list']) - formatted_url = data['next_page_url'] - - return recipe_list - - - -def user_history_page(): - - # 앱 헤더 - page_header() - - # get data - recipe_list = get_and_stack_recipe_data() - - # show container - container = st.container(border=True) - - with container: - # title - st.markdown("

❤️ 내가 요리한 레시피 ❤️

", unsafe_allow_html=True) - display_ingredients_in_rows_of_four(recipe_list) - -# , signin_page, login_page, signin_page_2, signin_page_3, main_page_2 -menu_titles = ["house", "🛒이번주 장바구니 추천", "😋내가 요리한 레시피", "🔎취향저격 레시피", "Log In / 회원가입"] - -def main_page(): - container3_1 = stylable_container( - key="container_with_border", - css_styles=""" - { - border: 1px solid rgba(49, 51, 63, 0.2); - border-radius: 0.5rem; - padding: calc(1em - 1px); - } - """,) - with container3_1: - st.markdown("

나만의 식량 바구니에 \n 오신 것을 환영합니다!

", unsafe_allow_html=True) - st.markdown("

자신의 입맞에 맞는 레시피를 저장하고 \n 이번주에 구매할 식량 바구니를 추천받아보세요

", unsafe_allow_html=True) - - btn = sac.buttons( - items=['로그인', '회원가입'], - index=0, - format_func='title', - align='center', - direction='horizontal', - radius='lg', - return_index=False - ) - - container3_2 = st.container(border = True) - with container3_2: - st.markdown("

사용 방법

", unsafe_allow_html=True) - st.markdown("

회원 가입을 했을 때 어떤 기능을 쓸 수 있는지 살펴보는 페이지

", unsafe_allow_html=True) - left_co, cent_co,last_co = st.columns((1, 8, 1)) - with cent_co: - st.image('img/howto.png') - -def signin_page(): - container3 = st.container(border = True) - with container3: - st.markdown(f"

{menu_titles[4]}

", unsafe_allow_html=True) - - __login__obj = __login__( - auth_token = "courier_auth_token", - company_name = "Shims", - width = 200, height = 250, - logout_button_name = 'Logout', - hide_menu_bool = False, - hide_footer_bool = False) - - LOGGED_IN = __login__obj.build_login_ui() - - if LOGGED_IN == True: - st.markown("Your Streamlit Application Begins here!") - -def user_history_page(): - - recipe_list = get_and_stack_recipe_data() - - container = st.container(border=True) - - my_recipe_list = get_my_recipe_data() - - container3 = st.container(border=True) - with container3: - st.markdown("

최근에 만들었던 음식을 골라주세요

", unsafe_allow_html=True) - st.markdown("

5개 이상 선택해 주세요

", unsafe_allow_html=True) - cols2 = st.columns([4,4]) - - - col1, col2, col3, col4 = st.columns(4) - with col1: - with st.container(border=True): - my_recipe = my_recipe_list[0] - st.image(my_recipe["recipe_img_url"]) - st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) - with col2: - with st.container(border=True): - my_recipe = my_recipe_list[1] - st.image(my_recipe["recipe_img_url"]) - st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) - with col3: - with st.container(border=True): - my_recipe = my_recipe_list[2] - st.image(my_recipe["recipe_img_url"]) - st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) - with col4: - with st.container(border=True): - my_recipe = my_recipe_list[3] - st.image(my_recipe["recipe_img_url"]) - st.markdown(f"

{my_recipe['recipe_name']}

", unsafe_allow_html=True) - - btn = sac.buttons( - items=['더 보기', '다음 단계'], - index=0, - format_func='title', - align='center', - direction='horizontal', - radius='lg', - return_index=False, - ) - -def main_page_2(): - container3_1 = stylable_container( - key="container_with_border", - css_styles=""" - { - border: 1px solid rgba(49, 51, 63, 0.2); - border-radius: 0.5rem; - padding: calc(1em - 1px); - background-color: white; - } - """,) - with container3_1: - st.markdown("

나만의 식량 바구니에 \n 오신 것을 환영합니다!

", unsafe_allow_html=True) - st.markdown("

자신의 입맞에 맞는 레시피를 저장하고 \n 이번주에 구매할 식량 바구니를 추천받아보세요

", unsafe_allow_html=True) - - my_recipe_list = get_my_recipe_data() - # 취향저격 레시피 - container3_4 = st.container(border = True) - with container3_4: - st.markdown("

내가 해먹은 레시피

", unsafe_allow_html=True) - - col_1, col_2, col_3, col_4, col_5 = st.columns((1, 1, 1, 1, 1)) - - with col_1: - with st.container(border=True): - st.image(my_recipe_list[0]["recipe_img_url"]) - st.markdown(f"

{my_recipe_list[0]['recipe_name']}

", unsafe_allow_html=True) - with col_2: - with st.container(border=True): - st.image(my_recipe_list[1]["recipe_img_url"]) - st.markdown(f"

{my_recipe_list[1]['recipe_name']}

", unsafe_allow_html=True) - with col_3: - with st.container(border=True): - st.image(my_recipe_list[2]["recipe_img_url"]) - st.markdown(f"

{my_recipe_list[2]['recipe_name']}

", unsafe_allow_html=True) - with col_4: - with st.container(border=True): - #col_l, col_c, - st.image(my_recipe_list[3]["recipe_img_url"]) - st.markdown(f"

{my_recipe_list[3]['recipe_name']}

", unsafe_allow_html=True) - with col_5: - with st.container(border=True): - st.image(my_recipe_list[4]["recipe_img_url"]) - st.markdown(f"

{my_recipe_list[4]['recipe_name']}

", unsafe_allow_html=True) - - -def set_logout(): - st.session_state.user = None - st.session_state.is_authenticated = False - -def set_login(): - st.session_state.user = 'judy123' - st.session_state.is_authenticated = True - -def login_button(): - if st.session_state.is_authenticated: - login_button = st.button(f"{st.session_state.user}님 | 로그아웃", on_click=set_logout) - else: - login_button = st.button(f"회원가입 | 로그인", on_click=set_login) - - return login_button - -def page_header(): - cols = st.columns([8,3]) - with cols[0]: - st.header('나만의 식량 바구니') - with cols[-1]: - login_button() - button_css() - -def basket_feedback(): - st.markdown("
방금 추천받은 장바구니 어땠나요?
", unsafe_allow_html=True) - st.text("") - cols = st.columns([3,1,1,3]) - with cols[1]: - st.button('좋아요') - with cols[2]: - st.button('싫어요') - -def get_response(formatted_url): - response = requests.get(formatted_url) - if response.status_code == 200: - data = response.json() - else: - print(f'status code: {response.status_code}') - data = None - return data - -def patch_feedback(user_id, recipe_id, current_state): - url = api_prefix + "users/{user_id}/recipes/{recipe_id}/feedback" - data = { - 'feedback': not current_state - } - response = requests.patch(url.format(user_id=user_id, recipe_id=recipe_id), json=data) - print(f'status code: {response.status_code}') - st.rerun() - -def button_css(): - st.markdown( - """""", - unsafe_allow_html=True, - ) - - -st.set_page_config(layout="wide") - -# 세션 초기화 -if 'page_info' not in st.session_state: - st.session_state['page_info'] = 'home' - -# 로그인되어 있다고 가정 -def init(): - st.session_state.user = 1 - st.session_state.is_authenticated = True - -# 로그인 상태 초기화 -init() - - -# page_info 설정 -st.session_state['page_info'] = 'home' - - -app_title = "🛒 나만의 식량 바구니" -menu_titles = ["house", "🛒이번주 장바구니 추천", "😋내가 요리한 레시피", "🔎취향저격 레시피", "Log In / 회원가입", "MainPage-2", "User history"] -################ - -################ -# body -> main -> sub -container1 = st.container(border=True) -with container1: - cols = st.columns([1,2]) - with cols[0]: - st.markdown(f"

{app_title}

", unsafe_allow_html=True) - with cols[1]: - seg = sac.segmented( - items=[ - sac.SegmentedItem(icon=menu_titles[0]), - sac.SegmentedItem(label=menu_titles[1]), - sac.SegmentedItem(label=menu_titles[2]), - sac.SegmentedItem(label=menu_titles[3]), - sac.SegmentedItem(label=menu_titles[4]), - sac.SegmentedItem(label=menu_titles[5]), - sac.SegmentedItem(label=menu_titles[6]), - ], align='center', use_container_width=True, - ) - - container2 = st.container(border=True) - with container2: - if seg == menu_titles[1]: - # 🛒이번주 장바구니 추천 - container3 = st.container(border=True) - with container3: - st.markdown(f"

{menu_titles[1]}

", unsafe_allow_html=True) - - if 'page_info' not in st.session_state: - st.session_state['page_info'] = 'recommend' - - if st.session_state['page_info'] == 'result_page_1': - result_page_2() - else: - recommendation_page() - - elif seg == menu_titles[2]: - # 😋내가 요리한 레시피 - st.session_state['page_info'] = 'recommend_history' - main_page_2() - elif seg == menu_titles[3]: - st.session_state['page_info'] = 'recommend_history' - main_page_2() - elif seg == menu_titles[4]: - # 로그인 / 회원가입 - signin_page() - elif seg == menu_titles[5]: - # MainPage_2 - # main_page_2() - - st.session_state['page_info'] = "result_page_2" - result_page_2() - elif seg == menu_titles[6]: - - # show UserHistoryPage - st.session_state['page_info'] = 'user_history' - user_history_page() - else : - # Home - main_page() - \ No newline at end of file diff --git a/frontend/basket_login.py b/frontend/basket_login.py index 8310674..7783792 100644 --- a/frontend/basket_login.py +++ b/frontend/basket_login.py @@ -13,7 +13,7 @@ def login_request(user_id, password): headers = { 'Content-Type': 'application/json' } - + # print('>>>>> ', full_url) response = requests.post(full_url, headers=headers, json=request_body) response_json = response.json() if response.status_code == 200 else None diff --git a/frontend/basket_signup.py b/frontend/basket_signup.py index ece3ed3..c37b867 100644 --- a/frontend/basket_signup.py +++ b/frontend/basket_signup.py @@ -1,5 +1,3 @@ -# streamlit_app.py - import hmac import streamlit as st import requests diff --git a/frontend/common.py b/frontend/common.py index b3c1f36..e9f8beb 100644 --- a/frontend/common.py +++ b/frontend/common.py @@ -1,19 +1,20 @@ import random, string - import streamlit as st import streamlit_antd_components as sac random_chars = lambda: ''.join(random.choices(string.ascii_letters + string.digits, k=5)) +APP_SERVER_PUBLIC_IP = "175.45.194.96" +APP_SERVER_PRIVATE_IP = "localhost" # "10.0.7.7" def init(): st.session_state.is_authenticated = False - st.session_state.page_info = 'home' - st.session_state.url_prefix = 'http://localhost:8000' - st.session_state.url_main = 'http://175.45.194.96:8502/' + st.session_state.page_info = "home" + st.session_state.url_prefix = f"http://{APP_SERVER_PRIVATE_IP}:8000" + st.session_state.url_main = f"http://{APP_SERVER_PUBLIC_IP}:8501/" def set_logout_page(): st.session_state.is_authenticated = False - st.session_state.page_info = 'home' + st.session_state.page_info = "home" del st.session_state["password_correct"] def set_login_page(): @@ -26,27 +27,39 @@ def login_button(): cols = st.columns(2) if st.session_state.is_authenticated: with cols[0]: - st.write(f"{st.session_state.token['user_id']}님") + st.markdown(f"

{st.session_state.token['user_id']}

", unsafe_allow_html=True) + # st.write(f"{st.session_state.token['user_id']}님") with cols[1]: st.button(f"로그아웃", on_click=set_logout_page, key=f'logout_{st.session_state.page_info}_{random_chars()}') - else: with cols[0]: st.button(f"회원가입", on_click=set_signup_page, key=f'signup_{st.session_state.page_info}_{random_chars()}') with cols[1]: - st.button(f"로그인", on_click=set_login_page, key=f'login_{st.session_state.page_info}_{random_chars()}') + st.button(f"로그인", on_click=set_login_page, key=f'login_{st.session_state.page_info}_{random_chars()}', type='primary') return login_button def page_header(): - cols = st.columns([7,3]) + cols = st.columns([4, 1, 1, 1]) + + # 나만의 장바구니 with cols[0]: - # st.header('나만의 식량 바구니') st.markdown( - f'

나만의 식량 바구니

', + f'

나만의 장바구니 🛒

', unsafe_allow_html=True) - st.markdown( - '''''', + + ''', unsafe_allow_html=True) - with cols[-1]: - login_button() - - button_css() - def button_css(): st.markdown( """ ''', unsafe_allow_html=True) diff --git "a/frontend/pages/\354\236\245\353\260\224\352\265\254\353\213\210_\354\266\224\354\262\234\353\260\233\352\270\260_\360\237\222\230.py" "b/frontend/pages/2_\354\236\245\353\260\224\352\265\254\353\213\210_\354\266\224\354\262\234\353\260\233\352\270\260_\360\237\222\230.py" similarity index 100% rename from "frontend/pages/\354\236\245\353\260\224\352\265\254\353\213\210_\354\266\224\354\262\234\353\260\233\352\270\260_\360\237\222\230.py" rename to "frontend/pages/2_\354\236\245\353\260\224\352\265\254\353\213\210_\354\266\224\354\262\234\353\260\233\352\270\260_\360\237\222\230.py" diff --git "a/frontend/pages/AI\352\260\200_\354\204\240\354\240\225\355\225\234_\354\267\250\355\226\245\354\240\200\352\262\251_\353\240\210\354\213\234\355\224\274_\360\237\244\226.py" "b/frontend/pages/3_AI\352\260\200_\354\204\240\354\240\225\355\225\234_\354\267\250\355\226\245\354\240\200\352\262\251_\353\240\210\354\213\234\355\224\274_\360\237\244\226.py" similarity index 100% rename from "frontend/pages/AI\352\260\200_\354\204\240\354\240\225\355\225\234_\354\267\250\355\226\245\354\240\200\352\262\251_\353\240\210\354\213\234\355\224\274_\360\237\244\226.py" rename to "frontend/pages/3_AI\352\260\200_\354\204\240\354\240\225\355\225\234_\354\267\250\355\226\245\354\240\200\352\262\251_\353\240\210\354\213\234\355\224\274_\360\237\244\226.py" diff --git "a/frontend/pages/\353\202\264\352\260\200_\354\232\224\353\246\254\355\225\234_\353\240\210\354\213\234\355\224\274_\360\237\245\230.py" "b/frontend/pages/4_\353\202\264\352\260\200_\354\232\224\353\246\254\355\225\234_\353\240\210\354\213\234\355\224\274_\360\237\245\230.py" similarity index 100% rename from "frontend/pages/\353\202\264\352\260\200_\354\232\224\353\246\254\355\225\234_\353\240\210\354\213\234\355\224\274_\360\237\245\230.py" rename to "frontend/pages/4_\353\202\264\352\260\200_\354\232\224\353\246\254\355\225\234_\353\240\210\354\213\234\355\224\274_\360\237\245\230.py" diff --git a/frontend/result_page.py b/frontend/result_page.py index c52a09a..f225a1c 100644 --- a/frontend/result_page.py +++ b/frontend/result_page.py @@ -73,7 +73,6 @@ def result_page(): st.markdown("
AI 를 이용하여 당신의 입맛에 맞는 레시피와 필요한 식재료를 추천해줍니다.
", unsafe_allow_html=True) st.divider() - # 구매할 식료품 목록 st.markdown("

추천 장바구니

", unsafe_allow_html=True) diff --git a/frontend/signinpage_2.py b/frontend/signinpage_2.py index 5886cdf..76d656d 100644 --- a/frontend/signinpage_2.py +++ b/frontend/signinpage_2.py @@ -99,7 +99,7 @@ def choose_food_page(): st.error("⚠️추천 정확성을 위해 5개 이상 체크해주세요.") if len(next_page_url): - cols = st.columns([3,1,1,3]) + cols = st.columns([3,1,1.5,3]) with cols[1]: st.button('더보기', on_click=add_next_page_url, kwargs=dict(next_page_url=next_page_url)) with cols[2]: From 4c8f8e3dd03ee079d104d5bd15cb9ca6d8dbd2e1 Mon Sep 17 00:00:00 2001 From: GangBean Date: Thu, 28 Mar 2024 12:42:43 +0900 Subject: [PATCH 162/187] =?UTF-8?q?feat:=20=EC=83=88=EB=A1=9C=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=EC=8B=9C=20session=5Fstate=20key=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20#83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/common.py | 28 +++++++++++++++---- ...3\260\233\352\270\260_\360\237\222\230.py" | 11 +------- ...4\213\234\355\224\274_\360\237\244\226.py" | 25 ++++++++++------- ...4\213\234\355\224\274_\360\237\245\230.py" | 25 +++++++++-------- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/frontend/common.py b/frontend/common.py index e2b1a02..e6f40bb 100644 --- a/frontend/common.py +++ b/frontend/common.py @@ -1,16 +1,18 @@ import random, string import streamlit as st import streamlit_antd_components as sac +from streamlit_extras.switch_page_button import switch_page random_chars = lambda: ''.join(random.choices(string.ascii_letters + string.digits, k=5)) APP_SERVER_PUBLIC_IP = "175.45.194.96" APP_SERVER_PRIVATE_IP = "localhost" # "10.0.7.7" +URL_MAIN = f"http://{APP_SERVER_PUBLIC_IP}:8501/" def init(): st.session_state.is_authenticated = False st.session_state.page_info = "home" st.session_state.url_prefix = f"http://{APP_SERVER_PRIVATE_IP}:8000" - st.session_state.url_main = f"http://{APP_SERVER_PUBLIC_IP}:8501/" + st.session_state.url_main = URL_MAIN def set_logout_page(): st.session_state.is_authenticated = False @@ -25,16 +27,18 @@ def set_signup_page(): def login_button(): cols = st.columns(2) - if st.session_state.is_authenticated: + # if st.session_state.is_authenticated: + if st.session_state.get('is_authenticated', False): with cols[0]: st.markdown(f"

{st.session_state.token['user_id']} 님

", unsafe_allow_html=True) with cols[1]: st.button(f"로그아웃", on_click=set_logout_page, key=f'logout_{st.session_state.page_info}_{random_chars()}') else: with cols[0]: - st.button(f"회원가입", on_click=set_signup_page, key=f'signup_{st.session_state.page_info}_{random_chars()}') + # st.button(f"회원가입", on_click=set_signup_page, key=f'signup_{st.session_state.page_info}_{random_chars()}') + st.button(f"회원가입", on_click=set_signup_page, key=f'signup_{random_chars()}') with cols[1]: - st.button(f"로그인", on_click=set_login_page, key=f'login_{st.session_state.page_info}_{random_chars()}', type='primary') + st.button(f"로그인", on_click=set_login_page, key=f'login_{random_chars()}', type='primary') return login_button def page_header(): @@ -43,7 +47,8 @@ def page_header(): # 나만의 장바구니 with cols[0]: st.markdown( - f'

나만의 장바구니 🛒

', + # f'

나만의 장바구니 🛒

', + f'

나만의 장바구니 🛒

', unsafe_allow_html=True) # log in button @@ -57,7 +62,18 @@ def page_header(): button_css() link_css() display_css() - + +def back_to_home_container(): + with st.container(border=True): + cols = st.columns([3,2,2]) + with cols[1]: + st.write('로그인이 필요합니다.') + cols = st.columns([4,2.5,3]) + with cols[1]: + # st.link_button('메인페이지로 >>', st.session_state.url_main, type='primary') + if st.button('메인페이지로 >>', key='home', type='primary'): + switch_page('홈_🏠') + def link_css(): st.markdown( '''