This capability is a part of Redis Stack - Redis 内存数据库 Redis OM Python

了解如何使用 Redis Stack 和 Python 进行构建

Redis OM Python 是一款 Redis 客户端,它为管理 Redis 中的文档数据提供了高级抽象。本教程将向您展示如何使用 Redis OM Python、Redis Stack 和 Flask 微框架开始运行。

我们很乐意看到您使用 Redis Stack 和 Redis OM 构建的内容。加入 Redis 社区,在 Discord 上与我们聊聊有关 Redis OM 和 Redis Stack 的所有内容。在 我们的发布博客文章中阅读有关 Redis OM Python 的更多信息。

概述

此应用程序是使用 Flask 和简单域模型构建的 API,演示了使用 Redis OM 的常见数据操作模式。

我们的实体是 Person,具有以下 JSON 表示形式

{
  "first_name": "A string, the person's first or given name",
  "last_name": "A string, the person's last or surname",
  "age": 36,
  "address": {
    "street_number": 56,
    "unit": "A string, optional unit number e.g. B or 1",
    "street_name": "A string, name of the street they live on",
    "city": "A string, name of the city they live in",
    "state": "A string, state, province or county that they live in",
    "postal_code": "A string, their zip or postal code",
    "country": "A string, country that they live in."
  },
  "personal_statement": "A string, free text personal statement",
  "skills": [
    "A string: a skill the person has",
    "A string: another still that the person has"
  ]
}

我们将让 Redis OM 处理唯一 ID 的生成,它使用 ULID 来执行此操作。Redis OM 还将为我们处理唯一 Redis 键名称的创建,以及从存储在 Redis Stack 数据库中的 JSON 文档中保存和检索实体。

开始使用

要求

要运行此应用程序,您需要

获取源代码

从 GitHub 克隆存储库

$ git clone https://github.com/redis-developer/redis-om-python-flask-skeleton-app.git
$ cd redis-om-python-flask-skeleton-app

启动 Redis Stack 数据库或配置您的 Redis Cloud 凭据

接下来,我们将启动并运行一个 Redis Stack 数据库。如果您使用 Docker

$ docker-compose up -d
Creating network "redis-om-python-flask-skeleton-app_default" with the default driver
Creating redis_om_python_flask_starter ... done 

如果您使用 Redis Cloud,则需要数据库的主机名、端口号和密码。使用这些信息像这样设置 REDIS_OM_URL 环境变量

$ export REDIS_OM_URL=redis://default:<password>@<host>:<port>

(使用 Docker 时不需要此步骤,因为 Docker 容器在 localhost 端口 6379 上运行 Redis,并且没有密码,这是 Redis OM 使用的默认连接。)

例如,如果您的 Redis Cloud 数据库位于主机 enterprise.redis.com 上的端口 9139,并且您的密码是 5uper53cret,那么您将如下设置 REDIS_OM_URL

$ export REDIS_OM_URL=redis://default:5uper53cret@enterprise.redis.com:9139

创建 Python 虚拟环境并安装依赖项

创建一个 Python 虚拟环境,并安装项目依赖项,它们包括 FlaskRequests(仅在数据加载器脚本中使用)和 Redis OM

$ python3 -m venv venv
$ . ./venv/bin/activate
$ pip install -r requirements.txt

启动 Flask 应用程序

让我们在开发模式下启动 Flask 应用程序,这样,每次你在 app.py 中保存代码更改时,Flask 都会为你重新启动服务器

$ export FLASK_ENV=development
$ flask run

如果一切顺利,你应该会看到类似这样的输出

$ flask run
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: XXX-XXX-XXX

你现在已经启动并运行,并准备使用 Redis、搜索和查询、JSON 和 Redis OM for Python 对数据执行 CRUD 操作!为了确保服务器正在运行,将你的浏览器指向 http://127.0.0.1:5000/,在那里你应该可以看到应用程序的基本主页

screenshot

加载示例数据

我们提供了一些示例数据(位于 data/people.json 中)。Python 脚本 dataloader.py 通过将数据发布到应用程序的创建新人员端点,将每个人加载到 Redis 中。像这样运行它

$ python dataloader.py
Created person Robert McDonald with ID 01FX8RMR7NRS45PBT3XP9KNAZH
Created person Kareem Khan with ID 01FX8RMR7T60ANQTS4P9NKPKX8
Created person Fernando Ortega with ID 01FX8RMR7YB283BPZ88HAG066P
Created person Noor Vasan with ID 01FX8RMR82D091TC37B45RCWY3
Created person Dan Harris with ID 01FX8RMR8545RWW4DYCE5MSZA1

务必保留数据加载器输出的副本,因为你的 ID 将与教程中使用的 ID 不同。要继续执行,请用你的 ID 替换上面显示的 ID。例如,每当我们使用 Kareem Khan 时,将 01FX8RMR7T60ANQTS4P9NKPKX8 更改为你数据加载器在你的 Redis 数据库中分配给 Kareem 的 ID。

问题?

如果 Flask 服务器无法启动,请查看其输出。如果你看到类似这样的日志条目

raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 61 connecting to localhost:6379. Connection refused.

那么你需要启动 Redis Docker 容器(如果使用 Docker),或者设置 REDIS_OM_URL 环境变量(如果使用 Redis Cloud)。

如果你已设置 REDIS_OM_URL 环境变量,并且代码在启动时出现类似这样的错误

raise ConnectionError(self._error_message(e))
redis.exceptions.ConnectionError: Error 8 connecting to enterprise.redis.com:9139. nodename nor servname provided, or not known.

那么你需要检查在设置 REDIS_OM_URL 时是否使用了正确的域名、端口、密码和格式。

如果数据加载器无法将示例数据发布到应用程序中,请确保 Flask 应用程序在运行数据加载器之前正在运行。

创建、读取、更新和删除数据

让我们在 Redis 中创建和操作一些数据模型实例。这里我们将了解如何使用 curl 调用 Flask API(你也可以使用 Postman)、代码如何工作以及数据如何在 Redis 中存储。

使用 Redis OM 构建人员模型

Redis OM 允许我们使用 Python 类和 Pydantic 框架对实体进行建模。我们的人员模型包含在文件 person.py 中。以下是如何工作的说明

  • 我们声明一个扩展 Redis OM 类 JsonModel 的类 Person。这告诉 Redis OM 我们希望将这些实体作为 JSON 文档存储在 Redis 中。
  • 然后,我们在模型中声明每个字段,指定数据类型以及我们是否希望对该字段建立索引。例如,以下是 age 字段,我们已将其声明为我们希望建立索引的正整数
age: PositiveInt = Field(index=True)
  • skills 字段是一个字符串列表,声明如下
skills: List[str] = Field(index=True)
  • 对于 personal_statement 字段,我们不希望对字段值建立索引,因为它是一个自由文本句子,而不是一个单词或数字。为此,我们将告诉 Redis OM 我们希望能够对值执行全文搜索
personal_statement: str = Field(index=True, full_text_search=True)
  • address 的工作方式与其他字段不同。请注意,在我们的模型 JSON 表示中,地址是一个对象,而不是一个字符串或数字字段。使用 Redis OM,这被建模为一个二级类,它扩展了 Redis OM EmbeddedJsonModel
class Address(EmbeddedJsonModel):
    # field definitions...
  • EmbeddedJsonModel 中的字段以相同的方式定义,因此我们的类包含地址中每个数据项的字段定义。

  • 并非我们的 JSON 中的每个字段都存在于每个地址中,Redis OM 允许我们声明一个字段为可选字段,只要我们不对其建立索引即可

unit: Optional[str] = Field(index=False)
  • 我们还可以为字段设置默认值... 假设国家应为“英国”,除非另有说明
country: str = Field(index=True, default="United Kingdom")
  • 最后,要将嵌入式地址对象添加到我们的 Person 模型中,我们在 Person 类中声明一个类型为 Address 的字段
address: Address

添加新人员

app.py 中的函数 create_person 处理在 Redis 中创建新人员。它需要一个符合我们 Person 模型架构的 JSON 对象。然后使用该数据创建一个新的 Person 对象并将其保存在 Redis 中的代码很简单

  new_person = Person(**request.json)
  new_person.save()
  return new_person.pk

创建新的 Person 实例时,Redis OM 会为其分配一个唯一的 ULID 主键,我们可以通过 .pk 访问该主键。我们将该主键返回给调用者,以便他们知道他们刚刚创建的对象的 ID。

然后,将对象持久保存到 Redis 只需调用其上的 .save() 即可。

试一试... 在服务器运行时,使用 curl 添加一个新人员

curl --location --request POST 'http://127.0.0.1:5000/person/new' \
--header 'Content-Type: application/json' \
--data-raw '{
    "first_name": "Joanne",
    "last_name": "Peel",
    "age": 36,
    "personal_statement": "Music is my life, I love gigging and playing with my band.",
    "address": {
      "street_number": 56,
      "unit": "4A",
      "street_name": "The Rushes",
      "city": "Birmingham",
      "state": "West Midlands",
      "postal_code": "B91 6HG",
      "country": "United Kingdom"
    },
    "skills": [
      "synths",
      "vocals",
      "guitar"
    ]
}'

运行上述 curl 命令将返回分配给新创建人员的唯一 ULID ID。例如 01FX8SSSDN7PT9T3N0JZZA758G

检查 Redis 中的数据

让我们看看我们刚刚保存在 Redis 中的内容。使用 RedisInsight 或 redis-cli 连接到数据库,查看存储在键 :person.Person:01FX8SSSDN7PT9T3N0JZZA758G 中的值。这在 Redis 中存储为 JSON 文档,因此如果使用 redis-cli,您将需要以下命令

$ redis-cli
127.0.0.1:6379> json.get :person.Person:01FX8SSSDN7PT9T3N0JZZA758G

如果您使用 RedisInsight,当您单击键名时,浏览器将为您呈现键值

Data in RedisInsight

在 Redis 中将数据存储为 JSON 时,我们可以更新和检索整个文档,或仅检索其中的一部分。例如,要仅检索该人员的地址和第一项技能,请使用以下命令(RedisInsight 用户应为此使用内置的 redis-cli)

$ redis-cli
127.0.0.1:6379> json.get :person.Person:01FX8SSSDN7PT9T3N0JZZA758G $.address $.skills[0]
"{\"$.skills[0]\":[\"synths\"],\"$.address\":[{\"pk\":\"01FX8SSSDNRDSRB3HMVH00NQTT\",\"street_number\":56,\"unit\":\"4A\",\"street_name\":\"The Rushes\",\"city\":\"Birmingham\",\"state\":\"West Midlands\",\"postal_code\":\"B91 6HG\",\"country\":\"United Kingdom\"}]}"

有关用于查询 Redis 中 JSON 文档的 JSON 路径语法的更多信息,请参阅文档

按 ID 查找人员

如果我们知道一个人的 ID,我们可以检索其数据。app.py 中的函数 find_by_id 将 ID 作为其参数,并要求 Redis OM 使用 ID 和 Person .get 类方法检索并填充 Person 对象

  try:
      person = Person.get(id)
      return person.dict()
  except NotFoundError:
      return {}

.dict() 方法将我们的 Person 对象转换为 Python 字典,然后 Flask 将其返回给调用者。

请注意,如果 Redis 中没有具有所提供 ID 的 Person,get 将抛出 NotFoundError

使用 curl 尝试一下,将您刚刚在数据库中创建的人员的 ID 替换为 01FX8SSSDN7PT9T3N0JZZA758G

curl --location --request GET 'http://localhost:5000/person/byid/01FX8SSSDN7PT9T3N0JZZA758G'

服务器将响应一个包含用户数据的 JSON 对象

{
  "address": {
    "city": "Birmingham",
    "country": "United Kingdom",
    "pk": "01FX8SSSDNRDSRB3HMVH00NQTT",
    "postal_code": "B91 6HG",
    "state": "West Midlands",
    "street_name": "The Rushes",
    "street_number": 56,
    "unit": null
  },
  "age": 36,
  "first_name": "Joanne",
  "last_name": "Peel",
  "personal_statement": "Music is my life, I love gigging and playing with my band.",
  "pk": "01FX8SSSDN7PT9T3N0JZZA758G",
  "skills": [
    "synths",
    "vocals",
    "guitar"
  ]
}

查找具有匹配名和姓氏的人员

让我们找到所有具有给定名和姓氏的人员... 这由 app.py 中的函数 find_by_name 处理。

在这里,我们使用 Person 的 find 类方法,该方法由 Redis OM 提供。我们向其传递一个搜索查询,指定我们要查找其 first_name 字段包含传递给 find_by_namefirst_name 参数的值且其 last_name 字段包含传递给 last_name 参数的值的人员

  people = Person.find(
      (Person.first_name == first_name) &
      (Person.last_name == last_name)
  ).all()

.all() 告诉 Redis OM 我们要检索所有匹配的人员。

使用 curl 如下尝试一下

curl --location --request GET 'http://127.0.0.1:5000/people/byname/Kareem/Khan'

注意:名和姓区分大小写。

服务器响应一个包含 results 的对象,该对象是一个匹配项数组

{
  "results": [
    {
      "address": {
        "city": "Sheffield",
        "country": "United Kingdom",
        "pk": "01FX8RMR7THMGA84RH8ZRQRRP9", 
        "postal_code": "S1 5RE",
        "state": "South Yorkshire",
        "street_name": "The Beltway",
        "street_number": 1,
        "unit": "A"
      },
      "age": 27,
      "first_name": "Kareem",
      "last_name": "Khan",
      "personal_statement":"I'm Kareem, a multi-instrumentalist and singer looking to join a new rock band.",
      "pk":"01FX8RMR7T60ANQTS4P9NKPKX8",
      "skills": [
        "drums",
        "guitar",
        "synths"
      ]
    }
  ]
}

查找特定年龄范围内的用户

能够找到属于特定年龄范围的用户非常有用... app.py 中的 find_in_age_range 函数按如下方式处理此问题...

我们将再次使用 Person 的 find 类方法,这次向其传递最小年龄和最大年龄,指定我们仅需要 age 字段介于这些值之间的结果

  people = Person.find(
      (Person.age >= min_age) &
      (Person.age <= max_age)
  ).sort_by("age").all()

请注意,我们还可以使用 .sort_by 指定我们希望按哪个字段对结果进行排序。

让我们按年龄对 30 到 47 岁之间的所有人进行排序

curl --location --request GET 'http://127.0.0.1:5000/people/byage/30/47'

这将返回一个包含匹配项数组的 results 对象

{
  "results": [
    {
      "address": {
        "city": "Sheffield",
        "country": "United Kingdom",
        "pk": "01FX8RMR7NW221STN6NVRDPEDT",
        "postal_code": "S12 2MX",
        "state": "South Yorkshire",
        "street_name": "Main Street",
        "street_number": 9,
        "unit": null
      },
      "age": 35,
      "first_name": "Robert",
      "last_name": "McDonald",
      "personal_statement": "My name is Robert, I love meeting new people and enjoy music, coding and walking my dog.",
      "pk": "01FX8RMR7NRS45PBT3XP9KNAZH",
      "skills": [
        "guitar",
        "piano",
        "trombone"
      ]
    },
    {
      "address": {
        "city": "Birmingham",
        "country": "United Kingdom",
        "pk": "01FX8SSSDNRDSRB3HMVH00NQTT",
        "postal_code": "B91 6HG",
        "state": "West Midlands",
        "street_name": "The Rushes",
        "street_number": 56,
        "unit": null
      },
      "age": 36,
      "first_name": "Joanne",
      "last_name": "Peel",
      "personal_statement": "Music is my life, I love gigging and playing with my band.",
      "pk": "01FX8SSSDN7PT9T3N0JZZA758G",
      "skills": [
        "synths",
        "vocals",
        "guitar"
      ]
    },
    {
      "address": {
        "city": "Nottingham",
        "country": "United Kingdom",
        "pk": "01FX8RMR82DDJ90CW8D1GM68YZ",
        "postal_code": "NG1 1AA",
        "state": "Nottinghamshire",
        "street_name": "Broadway",
        "street_number": 12,
        "unit": "A-1"
      },
      "age": 37,
      "first_name": "Noor",
      "last_name": "Vasan",
      "personal_statement": "I sing and play the guitar, I enjoy touring and meeting new people on the road.",
      "pk": "01FX8RMR82D091TC37B45RCWY3",
      "skills": [
        "vocals",
        "guitar"
      ]
    },
    {
      "address": {
        "city": "San Diego",
        "country": "United States",
        "pk": "01FX8RMR7YCDAVSWBMWCH2B07G",
        "postal_code": "92102",
        "state": "California",
        "street_name": "C Street",
        "street_number": 1299,
        "unit": null
      },
      "age": 43,
      "first_name": "Fernando",
      "last_name": "Ortega",
      "personal_statement": "I'm in a really cool band that plays a lot of cover songs.  I'm the drummer!",
      "pk": "01FX8RMR7YB283BPZ88HAG066P",
      "skills": [
        "clarinet",
        "oboe",
        "drums"
      ]
    }
  ]
}

查找具有特定技能的特定城市中的用户

现在,我们将尝试一种稍微不同的查询类型。我们要找到居住在特定城市且还具备特定技能的所有用户。这需要同时搜索 city 字段(它是一个字符串)和 skills 字段(它是一个字符串数组)。

从本质上讲,我们希望说“找到所有城市为 city 且技能数组包含 desired_skill 的用户”,其中 citydesired_skillapp.pyfind_matching_skill 函数的参数。以下是该代码

  people = Person.find(
      (Person.skills << desired_skill) &
      (Person.address.city == city)
  ).all()

这里的 << 运算符用于表示“in”或“contains”。

让我们找到谢菲尔德的所有吉他手

curl --location --request GET 'http://127.0.0.1:5000/people/byskill/guitar/Sheffield'

注意: Sheffield 区分大小写。

服务器返回一个包含匹配用户的 results 数组

{
  "results": [
    {
      "address": {
        "city": "Sheffield",
        "country": "United Kingdom",
        "pk": "01FX8RMR7THMGA84RH8ZRQRRP9",
        "postal_code": "S1 5RE",
        "state": "South Yorkshire",
        "street_name": "The Beltway",
        "street_number": 1,
        "unit": "A"
      },
      "age": 28,
      "first_name": "Kareem",
      "last_name": "Khan",
      "personal_statement": "I'm Kareem, a multi-instrumentalist and singer looking to join a new rock band.",
      "pk": "01FX8RMR7T60ANQTS4P9NKPKX8",
      "skills": [
        "drums",
        "guitar",
        "synths"
      ]
    },
    {
      "address": {
        "city": "Sheffield",
        "country": "United Kingdom",
        "pk": "01FX8RMR7NW221STN6NVRDPEDT",
        "postal_code": "S12 2MX",
        "state": "South Yorkshire",
        "street_name": "Main Street",
        "street_number": 9,
        "unit": null
      },
      "age": 35,
      "first_name": "Robert",
      "last_name": "McDonald",
      "personal_statement": "My name is Robert, I love meeting new people and enjoy music, coding and walking my dog.",
      "pk": "01FX8RMR7NRS45PBT3XP9KNAZH",
      "skills": [
        "guitar",
        "piano",
        "trombone"
      ]
    }
  ]
}

使用全文搜索个人陈述来查找用户

每个人都有一个 personal_statement 字段,它是一个自由文本字符串,其中包含有关他们的几句话。我们选择以一种使其可全文搜索的方式对其进行索引,因此让我们看看如何使用它。此代码位于 app.py 中的 find_matching_statements 函数中。

要搜索 personal_statement 字段中具有参数 search_term 值的用户,我们使用 % 运算符

  Person.find(Person.personal_statement % search_term).all()

让我们找到所有在个人陈述中提到“play”的人。

curl --location --request GET 'http://127.0.0.1:5000/people/bystatement/play'

服务器使用匹配用户的 results 数组进行响应

{
  "results": [
    { 
      "address": {
        "city": "San Diego",
        "country": "United States",
        "pk": "01FX8RMR7YCDAVSWBMWCH2B07G",
        "postal_code": "92102",
        "state": "California",
        "street_name": "C Street",
        "street_number": 1299,
        "unit": null
      },
      "age": 43,
      "first_name": "Fernando",
      "last_name": "Ortega",
      "personal_statement": "I'm in a really cool band that plays a lot of cover songs.  I'm the drummer!",
      "pk": "01FX8RMR7YB283BPZ88HAG066P",
      "skills": [
        "clarinet",
        "oboe",
        "drums"
      ]
    }, {
      "address": {
        "city": "Nottingham",
        "country": "United Kingdom",
        "pk": "01FX8RMR82DDJ90CW8D1GM68YZ",
        "postal_code": "NG1 1AA",
        "state": "Nottinghamshire",
        "street_name": "Broadway",
        "street_number": 12,
        "unit": "A-1"
      },
      "age": 37,
      "first_name": "Noor",
      "last_name": "Vasan",
      "personal_statement": "I sing and play the guitar, I enjoy touring and meeting new people on the road.",
      "pk": "01FX8RMR82D091TC37B45RCWY3",
      "skills": [
        "vocals",
        "guitar"
      ]
    },
    {
      "address": {
        "city": "Birmingham",
        "country": "United Kingdom",
        "pk": "01FX8SSSDNRDSRB3HMVH00NQTT",
        "postal_code": "B91 6HG",
        "state": "West Midlands",
        "street_name": "The Rushes",
        "street_number": 56,
        "unit": null
      },
      "age": 36,
      "first_name": "Joanne",
      "last_name": "Peel",
      "personal_statement": "Music is my life, I love gigging and playing with my band.",
      "pk": "01FX8SSSDN7PT9T3N0JZZA758G",
      "skills": [
        "synths",
        "vocals",
        "guitar"
      ]
    }
  ]
}

请注意,我们得到的匹配结果包括“play”、“plays”和“playing”。

更新用户的年龄

除了从 Redis 中检索信息之外,我们还希望不时更新用户的资料。让我们看看如何使用 Redis OM for Python 来实现这一点。

app.py 中的 update_age 函数接受两个参数:idnew_age。使用这些参数,我们首先从 Redis 中检索用户的资料并使用它创建一个新对象

  try:
      person = Person.get(id)

  except NotFoundError:
      return "Bad request", 400

假设我们找到了该用户,让我们更新他们的年龄并将资料保存回 Redis

  person.age = new_age
  person.save()

让我们将 Kareem Khan 的年龄从 27 岁改为 28 岁

curl --location --request POST 'http://127.0.0.1:5000/person/01FX8RMR7T60ANQTS4P9NKPKX8/age/28'

服务器响应ok

删除人员

如果我们知道人员的 ID,我们可以在不先将他们的数据加载到人员对象中时从 Redis 中删除他们。在 app.py 中的 delete_person 函数中,我们调用人员类上的 delete 类方法来执行此操作

  Person.delete(id)

让我们删除 ID 为 01FX8RMR8545RWW4DYCE5MSZA1 的人员 Dan Harris

curl --location --request POST 'http://127.0.0.1:5000/person/01FX8RMR8545RWW4DYCE5MSZA1/delete'

无论提供的 ID 是否存在于 Redis 中,服务器都会响应ok

为人员设置过期时间

这是一个针对存储在 Redis 中的模型实例运行任意 Redis 命令的示例。让我们看看如何设置人员的生存时间 (TTL),以便 Redis 在经过可配置的秒数后使 JSON 文档过期。

app.py 中的 expire_by_id 函数按如下方式处理此问题。它采用两个参数:id - 要过期的人员的 ID,以及 seconds - 在人员过期后的未来秒数。这要求我们针对人员的键运行 Redis EXPIRE 命令。为此,我们需要像这样从 Person 模型访问 Redis 连接

  person_to_expire = Person.get(id)
  Person.db().expire(person_to_expire.key(), seconds)

让我们将 ID 为 01FX8RMR82D091TC37B45RCWY3 的人员设置为在 600 秒后过期

curl --location --request POST 'http://localhost:5000/person/01FX8RMR82D091TC37B45RCWY3/expire/600'

使用 redis-cli,您可以使用 Redis expire 命令检查人员现在是否设置了 TTL

127.0.0.1:6379> ttl :person.Person:01FX8RMR82D091TC37B45RCWY3
(integer) 584

这表明 Redis 将在 584 秒后使键过期。

您可以在模型类上使用 .db() 函数来获取底层 redis-py 连接,以便在需要时运行较低级别的 Redis 命令。有关更多详细信息,请参阅 redis-py 文档

关闭 Redis(Docker)

如果您使用的是 Docker,并且希望在完成应用程序后关闭 Redis 容器,请使用 docker-compose down

$ docker-compose down
Stopping redis_om_python_flask_starter ... done
Removing redis_om_python_flask_starter ... done
Removing network redis-om-python-flask-skeleton-app_default
对本页进行评分