发短信是现代社会最常见的通信方式之一。Twilio通过其SMS API提供了一个简单易用的界面来发送SMS短信。
在本教程中,你将使用Twilio SMS API,利用Python和FastAPI框架创建你自己的API短信服务。
此外,你还将学习如何测试用FastAPI创建的后端服务器,以及如何使用Docker将API部署到Heroku。
如果你不熟悉,FastAPI是一个用于创建快速API应用的Python网络框架。FastAPI还默认与Swagger文档集成,并使其易于配置和更新。
另一方面,Docker是软件工程中的一个行业支柱,因为它是目前最流行的容器化软件之一。Docker用于开发、部署和管理称为容器的虚拟化环境中的应用程序。
使用Docker的主要卖点是它解决了一个问题:"它在我的机器上有效,为什么在你的机器上不行?"。巧合的是,我在做这个项目时,实际上也遇到了这个问题,最终在我决定使用Docker时解决了这个问题。
最后,Heroku是一个云平台,你可以在这里部署、管理和扩展网络应用。它适用于后端应用、前端应用或全栈应用。
安装Twilio CLI,并进行设置
你首先需要安装和设置Twilio命令行界面(CLI)。对于Linux,设置命令如下:
$ sudo apt-get install sqlite3
$ sudo apt install -y twilio
$ twilio login
在Windows或MacOS上安装sqlite3的说明可以在这里找到。如果你是在MacOS或Windows上,请查看Twilio文档中的CLI快速入门,以安装Twilio CLI。
twilio login
你可以在Twilio控制台中看到,Twilio CLI会询问你的证书,并提示你为你的本地配置文件输入一个用户名。一旦你完成了这些,请运行以下程序:
$ twilio profiles:use username
如果你还没有购买Twilio的电话号码,你可以通过运行下面的命令行,并从产生的列表中挑选一个电话号码来完成:
$ twilio phone-numbers:buy:local --country-code US --sms-enabled
开发环境设置
首先,让我们通过安装所有需要的依赖项来建立我们的开发环境。创建一个名为twilio-sms-api
的新目录,然后,导航到该目录。
作为Python良好实践的一部分,你还应该创建一个虚拟环境。如果你在 UNIX 或 macOS 上工作,运行下面的命令来创建和激活一个虚拟环境:
python3 -m venv venv
source venv/bin/activate
然而,如果你在 Windows 上工作,请运行这些命令来代替:
python -m venv venv
venv\bin\activate
然后,在twilio-sms-api
目录中创建一个requirements.txt
文件,并输入以下内容:
fastapi==0.78.0
uvicorn==0.18.2
twilio==7.11.0
python-dotenv==0.20.0
然后,运行pip3 install -r requirements.txt
来安装依赖性。运行之后,你应该已经安装了FastAPI、Uvicorn、Python Twilio SDK,以及python-dotenv--我们将使用它来访问环境变量。
另外,运行pip3 install pytest
,以便安装pytest,我们将用它来测试应用的逻辑。
做完这些后,在twilio-sms-api
目录中创建一个名为.env
的文件,并添加以下内容:
TWILIO_ACCOUNT_SID='your-account-sid'
TWILIO_AUTH_TOKEN='your-auth-token'
你需要你的账户SID和auth token来与Twilio SDK对接。你可以在Twilio控制台的Account Info
下访问这些。另外,最好的做法是把这些环境变量等机密的东西放在.env
文件中,而不是放在你的代码库中。如果使用GitHub这样的平台,别忘了创建一个.gitignore
文件,并将.env
文件添加到该文件中。
使用Twilio SMS API发送消息
说完了这些,我们终于可以开始制作我们的应用程序了。首先,让我告诉你如何用Twilio SMS API轻松发送短信。在twilio-sms-api
目录下创建一个send_sms.py
文件,并加入以下内容:
import os
from twilio.rest import Client
from dotenv import load_dotenv
load_dotenv()
# Find your Account SID and Auth Token at twilio.com/console
account_sid = os.getenv('TWILIO_ACCOUNT_SID')
auth_token = os.getenv('TWILIO_AUTH_TOKEN')
client = Client(account_sid, auth_token)
message = client.messages.create(
body="Hello, from Twilio and Python!",
to="number-verified-in-your-twilio-account",
from_='number-you-bought-through-twilio-cli',
)
print(f"message: {message.body}")
print(f"sent from: {message.from_}")
print(f"sent to: {message.to}")
记得用E.164格式写的相应的电话号码替换突出显示的to
和from
占位符。另外,请参阅添加验证的电话号码,了解如何在你的Twilio账户中验证你的号码。
在上面的代码片段中,我们使用了load_dotenv()
函数,这样 Python 就知道要通过.env
文件来查找。为了与Twilio的API对接,我们需要使用Twilio客户端->Client
。在那里我们传入我们的account_sid
,以及我们在.env
文件中的auth_token
。
请注意,Twilio的试用账户被限制为只能向经过验证的电话号码发送消息。此外,我们只能通过我们通过CLI购买的电话号码发送短信,所有发送的短信将以Sent from a Twilio trial account
。对于我们的用例,这很好。
现在,通过在命令行中输入python3 send_sms.py
来运行该应用程序。然后,你应该在控制台看到打印的信息,以及在你的手机号码上收到一条短信。
FastAPI的快速介绍
现在,创建一个main.py
文件并加入以下内容:
import os
from fastapi import FastAPI, status
from fastapi.middleware.cors import CORSMiddleware
from uvicorn import run
app = FastAPI()
origins = ["*"]
methods = ["*"]
headers = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=methods,
allow_headers=headers
)
@app.get("/", status_code = status.HTTP_200_OK)
async def root():
return {"message": "Hello!"}
if __name__ == "__main__":
port = int(os.environ.get('PORT', 5000))
run(app, host="0.0.0.0", port=port)
好了,让我们来看看我们的root()
方法。正如你所看到的,我们在这里通过添加@app.get()
,利用了一个GET
的请求,然后接收一个必要的路径参数。由于这是根路径,我们只需添加"/"
作为路径。然后,我们可以选择添加一个status.HTTP_200_OK
,作为我们的status_code
,因为如果请求成功,我们期望收到的就是这个。
在这下面,我们有一个函数,将返回一些东西。我们编写FastAPI端点的模板将是:
@app.http_method("url_path", some_optional_stuffs)
async def functionName():
return something
运行命令python3 -m uvicorn main:app --reload
,将运行应用程序,并将监听我们在服务器上的变化。
另外,你也可以使用python3 main.py
,它将在5000端口运行应用程序,这也是最后3行代码的功劳。然而,这不会让应用程序监听我们所做的更改,所以你必须在每次想看到你的更改时重新运行该应用程序。
我们还添加了CORSMiddleware
,这实质上允许我们在不同的主机中访问API。也就是说,我们可以通过为它创建一个前端接口来进一步扩展这个应用程序。我们不会在这篇文章中讨论这个问题,但我把它放在这里,是为了防止你也想创建一个前端与API进行交互。
在浏览器中导航到应用程序正在运行的端口,你会得到这个:
{
"message": "Hello!"
}
测试FastAPI应用程序的简要介绍
在FastAPI中测试是非常直接的,因为它允许你直接使用Pytest
。特别是,我们使用TestClient
,并传入我们先前的app
变量。然后我们编写assert
语句来测试应用程序。
在twilio-sms-api
目录中创建一个tests
目录,并在里面创建一个test_main.py
文件。在那里,添加main.py
的测试:
from fastapi import status
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"message": "Hello!"}
这个简短的测试只是测试导航到我们的根端点是否返回预期的响应。如果你运行命令python3 -m pytest tests/test_main.py
,你应该看到测试通过。
创建发送SMS信息的端点
现在,让我们来创建发送短信的端点。我们将基本上把我们在send_sms.py
的代码移植到我们的main.py
文件中,该文件将包含端点。
在main.py
的顶部,用下面的新行来更新该文件:
import os
from fastapi import FastAPI, status, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from uvicorn import run
from twilio.rest import Client
from dotenv import load_dotenv
load_dotenv()
然后,在root
端点的下面,添加以下代码:
# Below the root endpoint
@app.post("/message/send", status_code = status.HTTP_201_CREATED)
async def post_message(toNumber: str, fromNumber: str, message: str):
if (toNumber == None or toNumber == "" or fromNumber == None or fromNumber == "" or message == None or message == ""):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing values for query parameters")
if (toNumber[0] != "+" or fromNumber[0] != "+"):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Numbers must have a + sign in front")
account_sid = os.getenv("TWILIO_ACCOUNT_SID")
auth_token = os.getenv("TWILIO_AUTH_TOKEN")
if (account_sid == None and auth_token == None):
error_detail = "Missing values for TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN\n" + "SID: " + account_sid + "\n" + "Token: " + auth_token
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_detail)
elif (account_sid == None):
error_detail = "Missing value for TWILIO_ACCOUNT_SID\n" + "SID: " + account_sid
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_detail)
elif (auth_token == None):
error_detail = "Missing value for TWILIO_AUTH_TOKEN\n" + "Token: " + auth_token
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_detail)
client = Client(account_sid, auth_token)
clientMessage = client.messages.create(
body=message,
to=toNumber,
from_=fromNumber,
)
return {
"toNumber": toNumber,
"fromNumber": fromNumber,
"message": message,
"messageBody": clientMessage.body,
}
...
...
...
现在,在这里我们有一个POST
请求,因为我们将向服务器发送信息,然后服务器将返回一些东西给我们。我们加入了三个必要的查询参数,这是我们发送短信时需要的变量。你还会看到,我们有一些raise HTTPException()
语句。这里我们只是检查一些简单的边缘情况,这些情况会使我们的应用程序不能按预期工作(返回一个Internal Server Error
)。还有很多边缘情况需要处理,但对于我们的使用情况,这些就可以了。
一旦我们通过了错误检查,我们就可以创建我们的TwilioClient
,并发送一个带有查询参数值的消息。与send_sms.py
,你必须使用你从Twilio购买的号码,以及你用来验证Twilio账户的号码。
然后我们只是简单地返回一个JSON响应,其中包括传递的查询参数,以及Twilio发送的短信。
测试发送短信的端点
现在让我们测试一下我们创建的端点。在test_main.py
,添加以下测试:
# below tests for root endpoint
def test_post_message_success():
toNumber = "%2B" + "your-number-verified-with-twilio"
fromNumber = "%2B" + "twilio-number-you-bought"
toNumberExpected = "+" + "your-number-verified-with-twilio"
fromNumberExpected = "+" + "twilio-number-you-bought"
message = "Hello, from Twilio and Python!"
messageBodyExpected = "Sent from your Twilio trial account - Hello, from Twilio and Python!"
response = client.post("/message/send?toNumber=" + toNumber + "&fromNumber=" + fromNumber + "&message=" + message)
assert response.status_code == status.HTTP_201_CREATED
assert response.json() == {
"toNumber": toNumberExpected,
"fromNumber": fromNumberExpected,
"message": message,
"messageBody": messageBodyExpected,
}
def test_post_message_missing_all_query_parameters():
response = client.post("/message/send")
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_post_message_missing_query_parameter():
response = client.post("/message/send?fromNumber=01&message=Hello, from Twilio and Python!")
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_post_message_missing_values_query_parameters():
response = client.post("/message/send?toNumber=&fromNumber=&message=")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {"detail": "Missing values for query parameters"}
def test_post_message_missing_sign_from_number():
response = client.post("/message/send?toNumber=00&fromNumber=01&message=Hello, from Twilio and Python!")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {"detail": "Numbers must have a + sign in front"}
对于第一个测试,我们测试一个成功的POST请求到端点。在这里,我们期待一个201
状态代码,并且我们检查适当的JSON响应。同样,你将不得不替换高亮行中相应电话号码的占位符值。注意,Python不会自动对URL进行编码,所以我们用%2B
来代替+
符号。
注意,如果你已经升级了你的Twilio账户,你也要把messageBodyExpected
行改为messageBodyExpected = "Hello, from Twilio and Python!"
。
在接下来的两个测试中,我们检查是否有丢失的查询参数。由于这些查询参数是必需的,如果缺少一个,FastAPI会自动引发422
状态错误。
另一方面,最后两个测试是检查我们在main.py
文件中处理的一些边缘情况。
再次运行python3 -m pytest tests/test_main.py
,你应该看到,所有的测试都应该通过。
用Docker将应用程序部署到Heroku上
好了!现在我们的RESTful API已经在我们的本地主机上如期工作了。现在,我们要做的下一件事是使用Docker将这个API部署到Heroku。首先,在twilio-sms-api
目录中创建一个名为Dockerfile
的Docker文件:
FROM python:3.9.13-alpine
# Maintainer info
LABEL maintainer="your-email"
# Make working directories
RUN mkdir -p /twilio-sms-api
WORKDIR /twilio-sms-api
# Upgrade pip with no cache
RUN pip install --no-cache-dir -U pip
# Copy application requirements file to the created working directory
COPY requirements.txt .
# Install application dependencies from the requirements file
RUN pip install -r requirements.txt
# Copy every file in the source folder to the created working directory
COPY . .
# Run the python application
CMD ["python", "main.py"]
这将提取Python 3.9.13镜像,并安装所有在requirements.txt
文件中定义的必要包。然后,它将通过使用文件最后一行中定义的命令python main.py
,来运行应用程序。
确保你有Docker Desktop在运行,并且你已经登录了。然后,为你的应用程序想一个app-name
,比如sms-app
。你可以构建,然后使用以下CLI命令在5000端口上运行该应用程序:
$ docker image build -t <app-name> .
$ docker run -p 5000:5000 -d <app-name>
当运行上面的docker run
命令时,会返回一个container-id
。
然后,你可以停止该应用程序,并通过运行以下命令释放系统资源:
$ docker container stop <container-id>
$ docker system prune
现在应用已经Docker化了,我们现在可以把它部署到Heroku。
让我们首先通过CLI在Heroku中创建该应用:
$ heroku create <app-name>
然后,我们可以通过先前制作的Docker容器,用以下命令推送和发布该应用:
$ heroku container:push web --app <app-name>
$ heroku container:release web --app <app-name>
如果你在这一步遇到了 "没有基本认证 "的错误,这是因为你没有登录到Docker。你可以通过运行以下命令来登录。docker login --username=_ --password=$(heroku auth:token) registry.heroku.com
.重新运行上面的Heroku命令,它应该对你有效。
在这之后,你可以去你的Heroku仪表板,打开应用程序。你应该看到与下面类似的东西。
从这里,按下 "打开应用程序"按钮。你应该看到我们在应用程序的"/"
目录中的JSON信息。
导航到/docs
,你会看到应用程序的Swagger文档(默认情况下这是与FastAPI一起的)。在这里你可以玩玩我们创建的POST请求,看看它是否有效。
总结
在这篇文章中,你学到了如何使用Twilio SMS API发送短信,使用Python和FastAPI开发一个RESTful API,使用Pytest测试API,Dockerize整个应用程序,最后,将API部署到Heroku。