├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── Procfile ├── README.md ├── docker-compose.yml ├── nginx ├── Dockerfile └── nginx.conf ├── project ├── Dockerfile ├── config.py ├── gunicorn.py ├── line │ ├── message_event.py │ ├── urls.py │ └── user_event.py ├── main.py ├── poetry.lock ├── pyproject.toml └── view.py ├── requirements.txt └── runtime.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Pycharm related files 132 | .idea/ 133 | 134 | # MacOS related files 135 | **/.DS_Store 136 | 137 | # SSL file 138 | *.pem 139 | *.key 140 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "project/.venv/bin/python", 3 | "python.terminal.activateEnvInCurrentTerminal": true, 4 | "python.analysis.completeFunctionParens": true, 5 | "python.analysis.extraPaths": [ 6 | "project/" 7 | ], 8 | "git.ignoreLimitWarning": true 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 FawenYo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: uvicorn project.main:app --host=0.0.0.0 --port=${PORT:-5000} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LINE Bot Tutorial 2 | 3 | 鑑於網路上目前大部分 Python LINE Bot 教學多半使用 `Flask` 框架作為範例,但由於 `FastAPI` 非同步特性的優勢,目前趨勢已經開始從 `Flask` 往 `FastAPI` 移動。 4 | 本篇文章主要面向新手教學,使用 Python FastAPI 框架為範例,帶領大家做出自己的第一個 LINE Bot。 5 | 6 | ## 目標 7 | 8 | - 用 Python FastAPI 建立自己的第一個 echo bot (回聲機器人,Bot 接收到使用者訊息後會回傳一樣的內容給使用者) 9 | - 寫完程式碼後,將程式部署至 `Heroku` (免費) 或 自有主機 (可參考各大 VPS 主機商費用) 上架設伺服器並運行服務 10 | - 釐清 LINE Bot 中 `reply` 和 `push` 機制的差別 11 | 12 | ## 教學開始 13 | 14 | ### Step 1. 建立 LINE Bot 15 | 16 | 1. 進入 [LINE 控制台](https://developers.line.me/console/) 並輸入 LINE 帳號密碼登入 17 | ![image_1-1](https://i.imgur.com/W4Pkutn.png) 18 | 2. 創建服務提供者 19 | ![image_1-2](https://i.imgur.com/vPItzXs.png) 20 | 3. 輸入提供者名稱後,點擊 `Create` 21 | ![image_1-3](https://i.imgur.com/5ZY3TBB.png) 22 | 4. 選擇創建 `Messaging API` 23 | ![image_1-4](https://i.imgur.com/T2HWhjc.png) 24 | 5. 填入 LINE Bot 必要資訊,最後勾選同意條款,選擇 Create 創建機器人 25 | ![image_1-5](https://i.imgur.com/BalKUW4.png) 26 | 27 | 至此,我們已經創建好了 LINE Bot,但它目前還不能動,我們還要寫一些些的程式碼才能讓它動起來 28 | 29 | ## Step 2. LINE Bot 程式碼 30 | 31 | 1. 下載 [範例程式碼](https://github.com/FawenYo/LineBot_Tutorial/archive/master.zip) 32 | 2. 進入 [LINE 控制台](https://developers.line.me/console/),選擇剛剛創建的機器人 33 | ![image_2-1](https://i.imgur.com/Kq0flPc.png) 34 | 3. 進入 `Messaging API` 選項, 選擇 `Edit` 關閉 `Auto-reply messages` 選項 35 | ![image_2-2](https://i.imgur.com/ONZqF9D.png) 36 | 4. 取得 `Channel access token`,並先複製至筆記本中,等等會用到 37 | ![image_2-3](https://i.imgur.com/IjWyMVG.png) 38 | 5. 回到 `Basic Settings` 選項, 取得 `Channel secret`,也先將內容複製 39 | ![image_2-4](https://i.imgur.com/dW87qdV.png) 40 | 6. 使用編輯器開啟範例程式碼,點開 `project` 資料夾並新增檔案 `.env`,並貼上內容: 41 | 42 | ```env= 43 | LINE_CHANNEL_SECRET = "" 44 | LINE_CHANNEL_ACCESS_TOKEN = "" 45 | ``` 46 | 47 | 將剛剛複製的 `Channel secret` 和 `Channel access token` 分別填入 48 | ![image_2-5](https://i.imgur.com/FvaQWFV.png) 49 | 50 | 至此已完成 LINE Bot 程式碼的部分,其中與 LINE Bot 相關的內容都在 `projects` 資料夾目錄下,這邊也以功能性做區隔,方便日後修改的可維護性。 51 | 52 | ## Step. 3 部署 53 | 54 | 這邊將會分別介紹 `Heroku` 和 自有主機 的部署方式,新手會較推薦使用 `Heroku` 服務,操作較為簡單,且可免費使用。 55 | 56 | ### Heroku 57 | 58 | #### 簡介 59 | 60 | Heroku 是一個免費的雲端服務平台,只需寫好程式,Heroku 就會自己幫您自動佈建服務至雲端伺服器。 61 | 62 | 在 Heroku 中, app 可以將想作為 "服務" 的概念,其內容就是我們教學中的 "程式"。 63 | 64 | #### 創建 Heroku App 65 | 66 | 1. 登入 [Heroku](https://dashboard.heroku.com/apps) 後,點選 `New` => `Create New App` 67 | ![image_3-1](https://i.imgur.com/A9knwmd.png) 68 | 2. 設定 `App name` (未來服務會以該名稱為網址,也不能完全亂取名)後,點擊 `Create app` 69 | ![image_3-2](https://i.imgur.com/LrdGVsl.png) 70 | 71 | #### 部署至 Heroku 平台 72 | 73 | 1. 下載並安裝 [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli)、[Git](https://git-scm.com/) 74 | 2. 打開終端機至範例程式碼路徑 75 | 3. 登入至 Heroku 76 | 77 | ```bash 78 | heroku login 79 | ``` 80 | 81 | 4. 初始化 git (若過去有設定過 git 就不用再操作一次) 82 | 83 | ```bash 84 | git config --global user.name "您的名字" 85 | git config --global user.email "您的信箱" 86 | ``` 87 | 88 | 注意:`您的名字` 和 `您的信箱` 要換為自己的資訊 89 | 90 | 5. 將專案與 Heroku 連接 91 | 92 | ```bash 93 | heroku git:remote -a {HEROKU_APP_NAME} 94 | ``` 95 | 96 | **注意:`{HEROKU_APP_NAME}` 是前些步驟中我們為 Heroku App 設定的名字** 97 | 98 | 6. 將程式碼部署至 Heroku 99 | 100 | ```bash 101 | git add . 102 | git commit -m "Add code" 103 | git push heroku master 104 | ``` 105 | 106 | **往後要更新 Bot 時,只需重複這步驟指令就可以了** 107 | 108 | ### 自有主機 109 | 110 | 這邊我們以 Linux 的 `Debian` 系統作為示範,並假設使用者已經先行購買網域並複製憑證 `certificate.pem` 和 金鑰 `private.key` 檔案至 `nginx` 目錄下。 111 | 以下未特別說明的動作皆是在遠端主機下操作。 112 | 113 | 1. 安裝 `Git` 114 | 115 | ```bash 116 | sudo apt-get install git 117 | ``` 118 | 119 | 2. 安裝 `curl` 和 `Docker` 120 | 121 | ```bash 122 | sudo apt-get install -y curl 123 | curl -s https://get.docker.com | sudo sh 124 | ``` 125 | 126 | 3. 安裝 `docker-compose` 127 | 128 | ```bash 129 | sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 130 | 131 | sudo chmod +x /usr/local/bin/docker-compose 132 | ``` 133 | 134 | 4. 將範例程式碼先推送至個人 GitHub repo (個人本機) 135 | 136 | ```bash 137 | git add . 138 | git commit -m "Add code" 139 | git push origin master 140 | ``` 141 | 142 | 5. 將 GitHub repo 下載至主機 143 | 144 | ```bash 145 | git clone https://github.com/你的名字/專案名稱.git 146 | ``` 147 | 148 | 請記得將範例自行更改 149 | 150 | 6. 部署服務 151 | 152 | ```bash 153 | cd 專案名稱 154 | docker-compose up --build 155 | ``` 156 | 157 | ## Step. 4 連結服務與 LINE Bot 158 | 159 | 服務已經部署至 `Heroku` 或 自有主機,我們接著只需要將服務與 LINE Bot 連結便能完成任務了! 160 | 161 | 1. 前往 [Line OA](https://manager.line.biz/),選擇剛剛創建的 Bot 後點擊右上角設定 162 | ![image_4-1](https://i.imgur.com/ZJbfZdU.jpg) 163 | 2. 在 `Messaging API` 選項裡, `Webhook URL` 中輸入 "{網域}/callback",這邊以 `Heroku` 作為示範 164 | 165 | ```shell 166 | {HEROKU_APP_NAME}.herokuapp.com/callback 167 | ``` 168 | 169 | ![image_4-2](https://i.imgur.com/B7pKfYe.png) 170 | 注意:{HEROKU_APP_NAME} 是 Heroku App 的名稱 171 | 172 | 3. 在 `回應設定` 內的 `進階設定` 中,開啟 `Webhook` 服務 173 | ![image_4-3](https://i.imgur.com/EJucy5P.png) 174 | 4. 返回首頁並點擊 `加入好友指南` 後掃碼加入 LINE Bot,恭喜您完成了自己的第一個 LINE Bot! 試著跟它說話看看吧,它會回覆你喔! 175 | ![image_4-4](https://i.imgur.com/a6UI9dM.jpg) 176 | 177 | ## Debugging 178 | 179 | 如果程式遇到了問題,可以透過查看日誌 (log) 的方式找出錯誤。要查看 Bot 在 Heroku 的日誌,請按照以下步驟。 180 | 181 | 1. 如果沒登入,請先透過 Heroku CLI 登入 182 | 183 | ```shell 184 | heroku login 185 | ``` 186 | 187 | 2. 顯示 app 日誌 188 | 189 | ```shell 190 | heroku logs --tail --app {HEROKU_APP_NAME} 191 | ``` 192 | 193 | 注意:{HEROKU_APP_NAME} 是 Heroku App 名稱。 194 | 195 | ## 常見 Q&A 196 | 197 | - > Q: 有些文章中說 LINE Bot 訊息要錢,有些又說不用,讓人困惑 198 | 199 | A: LINE Bot 訊息可以分為 `reply` 和 `push` 兩種類型。 `reply` 是「回覆」使用者訊息,意思是使用者傳送訊息後的 30 秒內回覆內容,`reply` 訊息是完全免費的,並沒有任何收費。 `push` 則是「推播」訊息,譬如廣告帳號通送消息都是屬於 `push`,而免費版 LINE Bot 一個月有 **500則訊息額度** ,超過後依照用量計價。 200 | 201 | - > Q: 如果我想 push 訊息,但不想付錢怎麼辦QQ 202 | 203 | A: 個人目前方法是使用 `LINE Notify` 服務,透過該服務可以免費 push 無限數量的訊息或圖片,但沒有辦法傳送 template 類別 (ex. Flex message) 訊息。之後或許會考慮寫篇 `LINE Notify` 的教學文章。 204 | 205 | ## 進階操作 206 | 207 | [LINE GitHub Python SDK](https://github.com/line/line-bot-sdk-python):這是 LINE 官方的 GitHub Python 項目,裡面包含了各種操作範例,有興趣可以花時間讀讀 208 | [LINE Messaging API](https://developers.line.biz/en/reference/messaging-api/):LINE Bot 的所有 API 文件,願意花些時間讀讀文件的話或許也能找到些有趣的用法。 若是想做特殊的應用強烈建議閱讀。 209 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | project: 5 | restart: always 6 | build: ./project 7 | container_name: project 8 | expose: 9 | - 8001 10 | volumes: 11 | - ./project:/app 12 | environment: 13 | - TZ=Asia/Taipei 14 | nginx: 15 | build: ./nginx 16 | container_name: nginx 17 | ports: 18 | - "80:80" 19 | - "443:443" 20 | depends_on: 21 | - project 22 | environment: 23 | - TZ=Asia/Taipei 24 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the Nginx image 2 | FROM nginx 3 | 4 | # Remove the default nginx.conf 5 | RUN rm /etc/nginx/conf.d/default.conf 6 | 7 | # Replace with our own nginx.conf 8 | COPY nginx.conf /etc/nginx/conf.d/ 9 | COPY certificate.pem /etc/nginx/certificate.pem 10 | COPY private.key /etc/nginx/private.key -------------------------------------------------------------------------------- /nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | proxy_set_header Host $http_host; 6 | proxy_redirect off; 7 | proxy_set_header X-Real-IP $remote_addr; 8 | proxy_set_header X-Scheme $scheme; 9 | proxy_set_header USE_X_FORWARDED_HOST True; 10 | proxy_connect_timeout 6000; 11 | proxy_read_timeout 6000; 12 | proxy_pass http://project:8001; 13 | } 14 | 15 | location ~* \.(jpg|jpeg|png|css|js)$ { 16 | expires -1; 17 | proxy_pass http://project:8001; 18 | } 19 | 20 | } 21 | 22 | server { 23 | listen 443 ssl; 24 | location / { 25 | proxy_redirect off; 26 | proxy_set_header X-Real-IP $remote_addr; 27 | proxy_set_header X-Scheme $scheme; 28 | proxy_set_header USE_X_FORWARDED_HOST True; 29 | proxy_connect_timeout 6000; 30 | proxy_read_timeout 6000; 31 | proxy_pass http://project:8001; 32 | } 33 | 34 | location ~* \.(jpg|jpeg|png|css|js)$ { 35 | expires -1; 36 | proxy_pass http://project:8001; 37 | } 38 | 39 | # 憑證與金鑰的路徑 40 | ssl_certificate /etc/nginx/certificate.pem; 41 | ssl_certificate_key /etc/nginx/private.key; 42 | } -------------------------------------------------------------------------------- /project/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.6-buster 2 | 3 | WORKDIR /app 4 | 5 | ADD . /app 6 | 7 | RUN pip install -r requirements.txt 8 | ENV run_env=docker 9 | CMD gunicorn -c gunicorn.py -k uvicorn.workers.UvicornWorker main:app -------------------------------------------------------------------------------- /project/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | # Load environment variables 6 | load_dotenv() 7 | 8 | # LINE Bot 設定 9 | LINE_CHANNEL_SECRET = os.environ.get("LINE_CHANNEL_SECRET") 10 | LINE_CHANNEL_ACCESS_TOKEN = os.environ.get("LINE_CHANNEL_ACCESS_TOKEN") 11 | -------------------------------------------------------------------------------- /project/gunicorn.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Gunicorn settings 4 | port = int(os.getenv("PORT", 8001)) 5 | bind = f"0.0.0.0:{port}" 6 | 7 | profile = os.getenv("profile", "production") 8 | if profile == "production": 9 | loglevel = "info" 10 | else: 11 | # Development 12 | loglevel = "debug" 13 | 14 | workers = 4 15 | threads = 4 -------------------------------------------------------------------------------- /project/line/message_event.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from linebot import LineBotApi 4 | from linebot.models import TextMessage, TextSendMessage 5 | 6 | sys.path.append(".") 7 | 8 | import config 9 | 10 | line_bot_api = LineBotApi(config.LINE_CHANNEL_ACCESS_TOKEN) 11 | 12 | 13 | def handle_message(event) -> None: 14 | """Event - User sent message 15 | 16 | Args: 17 | event (LINE Event Object): Refer to https://developers.line.biz/en/reference/messaging-api/#message-event 18 | """ 19 | reply_token = event.reply_token 20 | 21 | # Text message 22 | if isinstance(event.message, TextMessage): 23 | # Get user sent message 24 | user_message = event.message.text 25 | 26 | # Reply with same message 27 | messages = TextSendMessage(text=user_message) 28 | line_bot_api.reply_message(reply_token=reply_token, messages=messages) -------------------------------------------------------------------------------- /project/line/urls.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from fastapi import APIRouter, HTTPException, Request 4 | from linebot import LineBotApi, WebhookHandler 5 | from linebot.exceptions import InvalidSignatureError 6 | from linebot.models import * 7 | 8 | from . import message_event, user_event 9 | 10 | sys.path.append(".") 11 | 12 | import config 13 | 14 | line_bot_api = LineBotApi(config.LINE_CHANNEL_ACCESS_TOKEN) 15 | handler = WebhookHandler(config.LINE_CHANNEL_SECRET) 16 | 17 | line_app = APIRouter() 18 | 19 | 20 | @line_app.post("/callback") 21 | async def callback(request: Request) -> str: 22 | """LINE Bot webhook callback 23 | 24 | Args: 25 | request (Request): Request Object. 26 | 27 | Raises: 28 | HTTPException: Invalid Signature Error 29 | 30 | Returns: 31 | str: OK 32 | """ 33 | signature = request.headers["X-Line-Signature"] 34 | body = await request.body() 35 | 36 | # handle webhook body 37 | try: 38 | handler.handle(body.decode(), signature) 39 | except InvalidSignatureError: 40 | raise HTTPException(status_code=400, detail="Missing Parameter") 41 | return "OK" 42 | 43 | 44 | @handler.add(FollowEvent) 45 | def handle_follow(event) -> None: 46 | """Event - User follow LINE Bot 47 | 48 | Args: 49 | event (LINE Event Object): Refer to https://developers.line.biz/en/reference/messaging-api/#follow-event 50 | """ 51 | user_event.handle_follow(event=event) 52 | 53 | 54 | @handler.add(UnfollowEvent) 55 | def handle_unfollow(event) -> None: 56 | """Event - User ban LINE Bot 57 | 58 | Args: 59 | event (LINE Event Object): Refer to https://developers.line.biz/en/reference/messaging-api/#unfollow-event 60 | """ 61 | user_event.handle_unfollow(event=event) 62 | 63 | 64 | @handler.add(MessageEvent, message=(TextMessage)) 65 | def handle_message(event) -> None: 66 | """Event - User sent message 67 | 68 | Args: 69 | event (LINE Event Object): Refer to https://developers.line.biz/en/reference/messaging-api/#message-event 70 | """ 71 | message_event.handle_message(event=event) 72 | -------------------------------------------------------------------------------- /project/line/user_event.py: -------------------------------------------------------------------------------- 1 | def handle_follow(event) -> None: 2 | """Event - User follow LINE Bot 3 | 4 | Args: 5 | event (LINE Event Object): Refer to https://developers.line.biz/en/reference/messaging-api/#follow-event 6 | """ 7 | user_id = event.source.user_id 8 | print(f"User follow! user_id: {user_id}") 9 | 10 | 11 | def handle_unfollow(event) -> None: 12 | """Event - User ban LINE Bot 13 | 14 | Args: 15 | event (LINE Event Object): Refer to https://developers.line.biz/en/reference/messaging-api/#unfollow-event 16 | """ 17 | user_id = event.source.user_id 18 | print(f"User leave! user_id: {user_id}") -------------------------------------------------------------------------------- /project/main.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | import uvicorn 5 | from fastapi import FastAPI 6 | from fastapi.staticfiles import StaticFiles 7 | from fastapi.templating import Jinja2Templates 8 | from line.urls import line_app 9 | from view import view 10 | 11 | app = FastAPI() 12 | 13 | templates = Jinja2Templates(directory="templates") 14 | 15 | app.mount("/static", StaticFiles(directory="static"), name="static") 16 | # View 17 | app.include_router(view) 18 | # LINE Bot 19 | app.include_router(line_app) 20 | 21 | 22 | if __name__ == "__main__": 23 | # Local WSGI: Uvicorn 24 | port = int(os.getenv("PORT", 8001)) 25 | uvicorn.run( 26 | "main:app", 27 | host="0.0.0.0", 28 | port=port, 29 | workers=4, 30 | log_level="info", 31 | access_log=True, 32 | use_colors=True, 33 | reload=True, 34 | ) -------------------------------------------------------------------------------- /project/poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "aiofiles" 3 | version = "0.6.0" 4 | description = "File support for asyncio." 5 | category = "main" 6 | optional = false 7 | python-versions = "*" 8 | 9 | [[package]] 10 | name = "appdirs" 11 | version = "1.4.4" 12 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 13 | category = "dev" 14 | optional = false 15 | python-versions = "*" 16 | 17 | [[package]] 18 | name = "black" 19 | version = "20.8b1" 20 | description = "The uncompromising code formatter." 21 | category = "dev" 22 | optional = false 23 | python-versions = ">=3.6" 24 | 25 | [package.dependencies] 26 | appdirs = "*" 27 | click = ">=7.1.2" 28 | mypy-extensions = ">=0.4.3" 29 | pathspec = ">=0.6,<1" 30 | regex = ">=2020.1.8" 31 | toml = ">=0.10.1" 32 | typed-ast = ">=1.4.0" 33 | typing-extensions = ">=3.7.4" 34 | 35 | [package.extras] 36 | colorama = ["colorama (>=0.4.3)"] 37 | d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] 38 | 39 | [[package]] 40 | name = "certifi" 41 | version = "2020.12.5" 42 | description = "Python package for providing Mozilla's CA Bundle." 43 | category = "main" 44 | optional = false 45 | python-versions = "*" 46 | 47 | [[package]] 48 | name = "chardet" 49 | version = "4.0.0" 50 | description = "Universal encoding detector for Python 2 and 3" 51 | category = "main" 52 | optional = false 53 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 54 | 55 | [[package]] 56 | name = "click" 57 | version = "7.1.2" 58 | description = "Composable command line interface toolkit" 59 | category = "main" 60 | optional = false 61 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 62 | 63 | [[package]] 64 | name = "fastapi" 65 | version = "0.63.0" 66 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 67 | category = "main" 68 | optional = false 69 | python-versions = ">=3.6" 70 | 71 | [package.dependencies] 72 | pydantic = ">=1.0.0,<2.0.0" 73 | starlette = "0.13.6" 74 | 75 | [package.extras] 76 | all = ["requests (>=2.24.0,<3.0.0)", "aiofiles (>=0.5.0,<0.6.0)", "jinja2 (>=2.11.2,<3.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<2.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "graphene (>=2.1.8,<3.0.0)", "ujson (>=3.0.0,<4.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)"] 77 | dev = ["python-jose[cryptography] (>=3.1.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.14.0)", "graphene (>=2.1.8,<3.0.0)"] 78 | doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=6.1.4,<7.0.0)", "markdown-include (>=0.5.1,<0.6.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.2.0)", "typer-cli (>=0.0.9,<0.0.10)", "pyyaml (>=5.3.1,<6.0.0)"] 79 | test = ["pytest (==5.4.3)", "pytest-cov (==2.10.0)", "pytest-asyncio (>=0.14.0,<0.15.0)", "mypy (==0.790)", "flake8 (>=3.8.3,<4.0.0)", "black (==20.8b1)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.15.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.4.0)", "orjson (>=3.2.1,<4.0.0)", "async_exit_stack (>=1.0.1,<2.0.0)", "async_generator (>=1.10,<2.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "aiofiles (>=0.5.0,<0.6.0)", "flask (>=1.1.2,<2.0.0)"] 80 | 81 | [[package]] 82 | name = "future" 83 | version = "0.18.2" 84 | description = "Clean single-source support for Python 3 and 2" 85 | category = "main" 86 | optional = false 87 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 88 | 89 | [[package]] 90 | name = "gunicorn" 91 | version = "20.1.0" 92 | description = "WSGI HTTP Server for UNIX" 93 | category = "main" 94 | optional = false 95 | python-versions = ">=3.5" 96 | 97 | [package.extras] 98 | eventlet = ["eventlet (>=0.24.1)"] 99 | gevent = ["gevent (>=1.4.0)"] 100 | setproctitle = ["setproctitle"] 101 | tornado = ["tornado (>=0.2)"] 102 | 103 | [[package]] 104 | name = "h11" 105 | version = "0.12.0" 106 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 107 | category = "main" 108 | optional = false 109 | python-versions = ">=3.6" 110 | 111 | [[package]] 112 | name = "httptools" 113 | version = "0.1.1" 114 | description = "A collection of framework independent HTTP protocol utils." 115 | category = "main" 116 | optional = false 117 | python-versions = "*" 118 | 119 | [package.extras] 120 | test = ["Cython (==0.29.14)"] 121 | 122 | [[package]] 123 | name = "idna" 124 | version = "2.10" 125 | description = "Internationalized Domain Names in Applications (IDNA)" 126 | category = "main" 127 | optional = false 128 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 129 | 130 | [[package]] 131 | name = "isort" 132 | version = "5.8.0" 133 | description = "A Python utility / library to sort Python imports." 134 | category = "dev" 135 | optional = false 136 | python-versions = ">=3.6,<4.0" 137 | 138 | [package.extras] 139 | pipfile_deprecated_finder = ["pipreqs", "requirementslib"] 140 | requirements_deprecated_finder = ["pipreqs", "pip-api"] 141 | colors = ["colorama (>=0.4.3,<0.5.0)"] 142 | 143 | [[package]] 144 | name = "jinja2" 145 | version = "2.11.3" 146 | description = "A very fast and expressive template engine." 147 | category = "main" 148 | optional = false 149 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 150 | 151 | [package.dependencies] 152 | MarkupSafe = ">=0.23" 153 | 154 | [package.extras] 155 | i18n = ["Babel (>=0.8)"] 156 | 157 | [[package]] 158 | name = "line-bot-sdk" 159 | version = "1.19.0" 160 | description = "LINE Messaging API SDK for Python" 161 | category = "main" 162 | optional = false 163 | python-versions = "*" 164 | 165 | [package.dependencies] 166 | future = "*" 167 | requests = ">=2.0" 168 | 169 | [[package]] 170 | name = "markupsafe" 171 | version = "1.1.1" 172 | description = "Safely add untrusted strings to HTML/XML markup." 173 | category = "main" 174 | optional = false 175 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" 176 | 177 | [[package]] 178 | name = "mypy-extensions" 179 | version = "0.4.3" 180 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 181 | category = "dev" 182 | optional = false 183 | python-versions = "*" 184 | 185 | [[package]] 186 | name = "pathspec" 187 | version = "0.8.1" 188 | description = "Utility library for gitignore style pattern matching of file paths." 189 | category = "dev" 190 | optional = false 191 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 192 | 193 | [[package]] 194 | name = "pydantic" 195 | version = "1.8.1" 196 | description = "Data validation and settings management using python 3.6 type hinting" 197 | category = "main" 198 | optional = false 199 | python-versions = ">=3.6.1" 200 | 201 | [package.dependencies] 202 | typing-extensions = ">=3.7.4.3" 203 | 204 | [package.extras] 205 | dotenv = ["python-dotenv (>=0.10.4)"] 206 | email = ["email-validator (>=1.0.3)"] 207 | 208 | [[package]] 209 | name = "python-dotenv" 210 | version = "0.17.0" 211 | description = "Read key-value pairs from a .env file and set them as environment variables" 212 | category = "main" 213 | optional = false 214 | python-versions = "*" 215 | 216 | [package.extras] 217 | cli = ["click (>=5.0)"] 218 | 219 | [[package]] 220 | name = "regex" 221 | version = "2021.4.4" 222 | description = "Alternative regular expression module, to replace re." 223 | category = "dev" 224 | optional = false 225 | python-versions = "*" 226 | 227 | [[package]] 228 | name = "requests" 229 | version = "2.25.1" 230 | description = "Python HTTP for Humans." 231 | category = "main" 232 | optional = false 233 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 234 | 235 | [package.dependencies] 236 | certifi = ">=2017.4.17" 237 | chardet = ">=3.0.2,<5" 238 | idna = ">=2.5,<3" 239 | urllib3 = ">=1.21.1,<1.27" 240 | 241 | [package.extras] 242 | security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] 243 | socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] 244 | 245 | [[package]] 246 | name = "starlette" 247 | version = "0.13.6" 248 | description = "The little ASGI library that shines." 249 | category = "main" 250 | optional = false 251 | python-versions = ">=3.6" 252 | 253 | [package.extras] 254 | full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"] 255 | 256 | [[package]] 257 | name = "toml" 258 | version = "0.10.2" 259 | description = "Python Library for Tom's Obvious, Minimal Language" 260 | category = "dev" 261 | optional = false 262 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 263 | 264 | [[package]] 265 | name = "typed-ast" 266 | version = "1.4.2" 267 | description = "a fork of Python 2 and 3 ast modules with type comment support" 268 | category = "dev" 269 | optional = false 270 | python-versions = "*" 271 | 272 | [[package]] 273 | name = "typing-extensions" 274 | version = "3.7.4.3" 275 | description = "Backported and Experimental Type Hints for Python 3.5+" 276 | category = "main" 277 | optional = false 278 | python-versions = "*" 279 | 280 | [[package]] 281 | name = "urllib3" 282 | version = "1.26.4" 283 | description = "HTTP library with thread-safe connection pooling, file post, and more." 284 | category = "main" 285 | optional = false 286 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" 287 | 288 | [package.extras] 289 | secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] 290 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 291 | brotli = ["brotlipy (>=0.6.0)"] 292 | 293 | [[package]] 294 | name = "uvicorn" 295 | version = "0.13.4" 296 | description = "The lightning-fast ASGI server." 297 | category = "main" 298 | optional = false 299 | python-versions = "*" 300 | 301 | [package.dependencies] 302 | click = ">=7.0.0,<8.0.0" 303 | h11 = ">=0.8" 304 | 305 | [package.extras] 306 | standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "httptools (>=0.1.0,<0.2.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] 307 | 308 | [[package]] 309 | name = "uvloop" 310 | version = "0.15.2" 311 | description = "Fast implementation of asyncio event loop on top of libuv" 312 | category = "main" 313 | optional = false 314 | python-versions = ">=3.7" 315 | 316 | [package.extras] 317 | dev = ["Cython (>=0.29.20,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)", "aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] 318 | docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"] 319 | test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] 320 | 321 | [metadata] 322 | lock-version = "1.1" 323 | python-versions = "3.8.6" 324 | content-hash = "990de730790c028cb04cbe29abd0727d54e07b2377aee70d91b51d027a2ae1a9" 325 | 326 | [metadata.files] 327 | aiofiles = [ 328 | {file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"}, 329 | {file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"}, 330 | ] 331 | appdirs = [ 332 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 333 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 334 | ] 335 | black = [ 336 | {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, 337 | ] 338 | certifi = [ 339 | {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, 340 | {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, 341 | ] 342 | chardet = [ 343 | {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, 344 | {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, 345 | ] 346 | click = [ 347 | {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, 348 | {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, 349 | ] 350 | fastapi = [ 351 | {file = "fastapi-0.63.0-py3-none-any.whl", hash = "sha256:98d8ea9591d8512fdadf255d2a8fa56515cdd8624dca4af369da73727409508e"}, 352 | {file = "fastapi-0.63.0.tar.gz", hash = "sha256:63c4592f5ef3edf30afa9a44fa7c6b7ccb20e0d3f68cd9eba07b44d552058dcb"}, 353 | ] 354 | future = [ 355 | {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, 356 | ] 357 | gunicorn = [ 358 | {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, 359 | ] 360 | h11 = [ 361 | {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, 362 | {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, 363 | ] 364 | httptools = [ 365 | {file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"}, 366 | {file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"}, 367 | {file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"}, 368 | {file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"}, 369 | {file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"}, 370 | {file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"}, 371 | {file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"}, 372 | {file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"}, 373 | {file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"}, 374 | {file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"}, 375 | {file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"}, 376 | {file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"}, 377 | ] 378 | idna = [ 379 | {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, 380 | {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, 381 | ] 382 | isort = [ 383 | {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, 384 | {file = "isort-5.8.0.tar.gz", hash = "sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6"}, 385 | ] 386 | jinja2 = [ 387 | {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, 388 | {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, 389 | ] 390 | line-bot-sdk = [ 391 | {file = "line-bot-sdk-1.19.0.tar.gz", hash = "sha256:11eec8dd5b7afd2fde6c0a1b8031d3aeb1dd19e18b9e36828ad90d1861afd689"}, 392 | {file = "line_bot_sdk-1.19.0-py2.py3-none-any.whl", hash = "sha256:41bd5cac4b353d9722173893bd99d88e32fd115a22655ad3b3e2040018a4f81c"}, 393 | ] 394 | markupsafe = [ 395 | {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, 396 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, 397 | {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, 398 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, 399 | {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, 400 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, 401 | {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, 402 | {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, 403 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, 404 | {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, 405 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, 406 | {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, 407 | {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, 408 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, 409 | {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, 410 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, 411 | {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, 412 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, 413 | {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, 414 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, 415 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, 416 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, 417 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, 418 | {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, 419 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, 420 | {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, 421 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, 422 | {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, 423 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, 424 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, 425 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, 426 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, 427 | {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, 428 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, 429 | {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, 430 | {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, 431 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, 432 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, 433 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, 434 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, 435 | {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, 436 | {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, 437 | {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, 438 | {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, 439 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, 440 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, 441 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, 442 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, 443 | {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, 444 | {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, 445 | {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, 446 | {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, 447 | ] 448 | mypy-extensions = [ 449 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 450 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 451 | ] 452 | pathspec = [ 453 | {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, 454 | {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, 455 | ] 456 | pydantic = [ 457 | {file = "pydantic-1.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0c40162796fc8d0aa744875b60e4dc36834db9f2a25dbf9ba9664b1915a23850"}, 458 | {file = "pydantic-1.8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fff29fe54ec419338c522b908154a2efabeee4f483e48990f87e189661f31ce3"}, 459 | {file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:fbfb608febde1afd4743c6822c19060a8dbdd3eb30f98e36061ba4973308059e"}, 460 | {file = "pydantic-1.8.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:eb8ccf12295113ce0de38f80b25f736d62f0a8d87c6b88aca645f168f9c78771"}, 461 | {file = "pydantic-1.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:20d42f1be7c7acc352b3d09b0cf505a9fab9deb93125061b376fbe1f06a5459f"}, 462 | {file = "pydantic-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dde4ca368e82791de97c2ec019681ffb437728090c0ff0c3852708cf923e0c7d"}, 463 | {file = "pydantic-1.8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3bbd023c981cbe26e6e21c8d2ce78485f85c2e77f7bab5ec15b7d2a1f491918f"}, 464 | {file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:830ef1a148012b640186bf4d9789a206c56071ff38f2460a32ae67ca21880eb8"}, 465 | {file = "pydantic-1.8.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:fb77f7a7e111db1832ae3f8f44203691e15b1fa7e5a1cb9691d4e2659aee41c4"}, 466 | {file = "pydantic-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:3bcb9d7e1f9849a6bdbd027aabb3a06414abd6068cb3b21c49427956cce5038a"}, 467 | {file = "pydantic-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2287ebff0018eec3cc69b1d09d4b7cebf277726fa1bd96b45806283c1d808683"}, 468 | {file = "pydantic-1.8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4bbc47cf7925c86a345d03b07086696ed916c7663cb76aa409edaa54546e53e2"}, 469 | {file = "pydantic-1.8.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:6388ef4ef1435364c8cc9a8192238aed030595e873d8462447ccef2e17387125"}, 470 | {file = "pydantic-1.8.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:dd4888b300769ecec194ca8f2699415f5f7760365ddbe243d4fd6581485fa5f0"}, 471 | {file = "pydantic-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:8fbb677e4e89c8ab3d450df7b1d9caed23f254072e8597c33279460eeae59b99"}, 472 | {file = "pydantic-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2f2736d9a996b976cfdfe52455ad27462308c9d3d0ae21a2aa8b4cd1a78f47b9"}, 473 | {file = "pydantic-1.8.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3114d74329873af0a0e8004627f5389f3bb27f956b965ddd3e355fe984a1789c"}, 474 | {file = "pydantic-1.8.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:258576f2d997ee4573469633592e8b99aa13bda182fcc28e875f866016c8e07e"}, 475 | {file = "pydantic-1.8.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:c17a0b35c854049e67c68b48d55e026c84f35593c66d69b278b8b49e2484346f"}, 476 | {file = "pydantic-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8bc082afef97c5fd3903d05c6f7bb3a6af9fc18631b4cc9fedeb4720efb0c58"}, 477 | {file = "pydantic-1.8.1-py3-none-any.whl", hash = "sha256:e3f8790c47ac42549dc8b045a67b0ca371c7f66e73040d0197ce6172b385e520"}, 478 | {file = "pydantic-1.8.1.tar.gz", hash = "sha256:26cf3cb2e68ec6c0cfcb6293e69fb3450c5fd1ace87f46b64f678b0d29eac4c3"}, 479 | ] 480 | python-dotenv = [ 481 | {file = "python-dotenv-0.17.0.tar.gz", hash = "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a"}, 482 | {file = "python_dotenv-0.17.0-py2.py3-none-any.whl", hash = "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"}, 483 | ] 484 | regex = [ 485 | {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, 486 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, 487 | {file = "regex-2021.4.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11"}, 488 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968"}, 489 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0"}, 490 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4"}, 491 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a"}, 492 | {file = "regex-2021.4.4-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7"}, 493 | {file = "regex-2021.4.4-cp36-cp36m-win32.whl", hash = "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29"}, 494 | {file = "regex-2021.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79"}, 495 | {file = "regex-2021.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8"}, 496 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31"}, 497 | {file = "regex-2021.4.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a"}, 498 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5"}, 499 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82"}, 500 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765"}, 501 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e"}, 502 | {file = "regex-2021.4.4-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439"}, 503 | {file = "regex-2021.4.4-cp37-cp37m-win32.whl", hash = "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d"}, 504 | {file = "regex-2021.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3"}, 505 | {file = "regex-2021.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500"}, 506 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14"}, 507 | {file = "regex-2021.4.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480"}, 508 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc"}, 509 | {file = "regex-2021.4.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093"}, 510 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10"}, 511 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f"}, 512 | {file = "regex-2021.4.4-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87"}, 513 | {file = "regex-2021.4.4-cp38-cp38-win32.whl", hash = "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac"}, 514 | {file = "regex-2021.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2"}, 515 | {file = "regex-2021.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17"}, 516 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605"}, 517 | {file = "regex-2021.4.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9"}, 518 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7"}, 519 | {file = "regex-2021.4.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8"}, 520 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed"}, 521 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c"}, 522 | {file = "regex-2021.4.4-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"}, 523 | {file = "regex-2021.4.4-cp39-cp39-win32.whl", hash = "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6"}, 524 | {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, 525 | {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, 526 | ] 527 | requests = [ 528 | {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, 529 | {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, 530 | ] 531 | starlette = [ 532 | {file = "starlette-0.13.6-py3-none-any.whl", hash = "sha256:bd2ffe5e37fb75d014728511f8e68ebf2c80b0fa3d04ca1479f4dc752ae31ac9"}, 533 | {file = "starlette-0.13.6.tar.gz", hash = "sha256:ebe8ee08d9be96a3c9f31b2cb2a24dbdf845247b745664bd8a3f9bd0c977fdbc"}, 534 | ] 535 | toml = [ 536 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 537 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 538 | ] 539 | typed-ast = [ 540 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, 541 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, 542 | {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, 543 | {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, 544 | {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, 545 | {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, 546 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, 547 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, 548 | {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, 549 | {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, 550 | {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, 551 | {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, 552 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, 553 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, 554 | {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, 555 | {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, 556 | {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, 557 | {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, 558 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, 559 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, 560 | {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, 561 | {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, 562 | {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, 563 | {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, 564 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, 565 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, 566 | {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, 567 | {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, 568 | {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, 569 | {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, 570 | ] 571 | typing-extensions = [ 572 | {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, 573 | {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, 574 | {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, 575 | ] 576 | urllib3 = [ 577 | {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, 578 | {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, 579 | ] 580 | uvicorn = [ 581 | {file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"}, 582 | {file = "uvicorn-0.13.4.tar.gz", hash = "sha256:3292251b3c7978e8e4a7868f4baf7f7f7bb7e40c759ecc125c37e99cdea34202"}, 583 | ] 584 | uvloop = [ 585 | {file = "uvloop-0.15.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69"}, 586 | {file = "uvloop-0.15.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7"}, 587 | {file = "uvloop-0.15.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d"}, 588 | {file = "uvloop-0.15.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c"}, 589 | {file = "uvloop-0.15.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47"}, 590 | {file = "uvloop-0.15.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c"}, 591 | {file = "uvloop-0.15.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc"}, 592 | {file = "uvloop-0.15.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760"}, 593 | {file = "uvloop-0.15.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c"}, 594 | {file = "uvloop-0.15.2.tar.gz", hash = "sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01"}, 595 | ] 596 | -------------------------------------------------------------------------------- /project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "LINE Bot Tutorial" 3 | version = "0.1.0" 4 | description = "FastAPI Example for LINE Bot" 5 | authors = ["FawenYo "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "3.8.6" 9 | fastapi = "^0.63.0" 10 | line-bot-sdk = "^1.19.0" 11 | uvicorn = "^0.13.4" 12 | python-dotenv = "^0.17.0" 13 | aiofiles = "^0.6.0" 14 | gunicorn = "^20.1.0" 15 | Jinja2 = "^2.11.3" 16 | uvloop = "^0.15.2" 17 | httptools = "^0.1.1" 18 | 19 | [tool.poetry.dev-dependencies] 20 | black = "^20.8b1" 21 | isort = "^5.8.0" 22 | 23 | [build-system] 24 | requires = ["poetry-core>=1.0.0"] 25 | build-backend = "poetry.core.masonry.api" 26 | -------------------------------------------------------------------------------- /project/view.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastapi.responses import JSONResponse 3 | from fastapi.templating import Jinja2Templates 4 | 5 | view = APIRouter() 6 | templates = Jinja2Templates(directory="templates") 7 | 8 | 9 | @view.get("/", response_class=JSONResponse) 10 | async def home() -> JSONResponse: 11 | """Home Page 12 | 13 | Returns: 14 | JSONResponse: Hello World! 15 | """ 16 | message = {"stauts": "success", "message": "Hello World!"} 17 | return JSONResponse(content=message) 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | gunicorn 3 | line-bot-sdk -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.8.6 --------------------------------------------------------------------------------