如何使用Twilio SMS和Python创建一个API短信服务

527 阅读2分钟

发短信是现代社会最常见的通信方式之一。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格式写的相应的电话号码替换突出显示的tofrom 占位符。另外,请参阅添加验证的电话号码,了解如何在你的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 目录中创建一个名为DockerfileDocker文件

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仪表板,打开应用程序。你应该看到与下面类似的东西。

Heroku dashboard

从这里,按下 "打开应用程序"按钮。你应该看到我们在应用程序的"/" 目录中的JSON信息。

JSON message greeting saying "Hello!" from Heroku app

导航到/docs ,你会看到应用程序的Swagger文档(默认情况下这是与FastAPI一起的)。在这里你可以玩玩我们创建的POST请求,看看它是否有效。

"/docs" page showing Swagger documentation of the Heroku app

总结

在这篇文章中,你学到了如何使用Twilio SMS API发送短信,使用Python和FastAPI开发一个RESTful API,使用Pytest测试API,Dockerize整个应用程序,最后,将API部署到Heroku。