附录 B:模板项目结构
在 [chapter_04_service_layer] 附近,我们从仅仅将所有内容放在一个文件夹中转变为更结构化的树形结构,并且我们认为概述一下各个组成部分可能会很有趣。
提示
|
此附录的代码位于 appendix_project_structure 分支 on GitHub git clone https://github.com/cosmicpython/code.git cd code git checkout appendix_project_structure |
基本文件夹结构如下所示
.
├── Dockerfile (1)
├── Makefile (2)
├── README.md
├── docker-compose.yml (1)
├── license.txt
├── mypy.ini
├── requirements.txt
├── src (3)
│ ├── allocation
│ │ ├── __init__.py
│ │ ├── adapters
│ │ │ ├── __init__.py
│ │ │ ├── orm.py
│ │ │ └── repository.py
│ │ ├── config.py
│ │ ├── domain
│ │ │ ├── __init__.py
│ │ │ └── model.py
│ │ ├── entrypoints
│ │ │ ├── __init__.py
│ │ │ └── flask_app.py
│ │ └── service_layer
│ │ ├── __init__.py
│ │ └── services.py
│ └── setup.py (3)
└── tests (4)
├── conftest.py (4)
├── e2e
│ └── test_api.py
├── integration
│ ├── test_orm.py
│ └── test_repository.py
├── pytest.ini (4)
└── unit
├── test_allocate.py
├── test_batches.py
└── test_services.py
-
我们的 docker-compose.yml 和 Dockerfile 是运行我们应用程序的容器的主要配置,它们也可以运行测试 (用于 CI)。更复杂的项目可能需要多个 Dockerfile,尽管我们发现尽量减少镜像的数量通常是一个好主意。[1]
-
Makefile 为开发人员(或 CI 服务器)在正常工作流程中可能想要运行的所有典型命令提供入口点:
make build
、make test
等。[2] 这是可选的。您可以直接使用docker-compose
和pytest
,但至少,最好将所有“常用命令”放在某处的列表中,并且与文档不同,Makefile 是代码,因此它不太可能过时。 -
我们应用程序的所有源代码,包括领域模型、Flask 应用程序和基础设施代码,都位于 src 中的 Python 包中,[3] 我们使用
pip install -e
和 setup.py 文件安装它。这使得导入变得容易。目前,此模块中的结构完全扁平,但对于更复杂的项目,您应该期望增长一个文件夹层次结构,其中包括 domain_model/、infrastructure/、services/ 和 api/。 -
测试位于它们自己的文件夹中。子文件夹区分不同的测试类型,并允许您分别运行它们。我们可以将共享 fixtures (conftest.py) 保留在主 tests 文件夹中,并在需要时嵌套更具体的 fixtures。这也是保留 pytest.ini 的位置。
提示
|
pytest 文档在测试布局和可导入性方面非常出色。 |
让我们更详细地查看其中一些文件和概念。
环境变量、12-Factor 以及容器内外配置
我们在这里尝试解决的基本问题是,我们需要以下不同的配置设置
-
直接从您自己的开发机器运行代码或测试,可能与来自 Docker 容器的映射端口通信
-
在容器本身上运行,使用“真实”端口和主机名
-
不同的容器环境(开发、暂存、生产等)
通过环境变量进行配置,正如 12-factor manifesto 建议的那样,将解决此问题,但具体而言,我们如何在我们的代码和容器中实现它?
Config.py
每当我们的应用程序代码需要访问某些配置时,它都会从名为 config.py 的文件中获取它。以下是我们应用程序中的几个示例
import os
def get_postgres_uri(): #(1)
host = os.environ.get("DB_HOST", "localhost") #(2)
port = 54321 if host == "localhost" else 5432
password = os.environ.get("DB_PASSWORD", "abc123")
user, db_name = "allocation", "allocation"
return f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
def get_api_url():
host = os.environ.get("API_HOST", "localhost")
port = 5005 if host == "localhost" else 80
return f"http://{host}:{port}"
-
我们使用函数来获取当前配置,而不是在导入时可用的常量,因为这允许客户端代码在需要时修改
os.environ
。 -
config.py 还定义了一些默认设置,旨在在从开发人员的本地机器运行代码时工作。[4]
如果您厌倦了手动编写自己的基于环境的配置函数,那么值得关注一个名为 environ-config 的优雅 Python 包。
提示
|
不要让这个 config 模块成为一个垃圾场,里面充满了仅与配置略微相关的东西,然后被导入到各处。保持事物不可变,并且仅通过环境变量修改它们。如果您决定使用 bootstrap 脚本,您可以使其成为唯一导入配置的位置(测试除外)。 |
Docker-Compose 和容器配置
我们使用一个名为 docker-compose 的轻量级 Docker 容器编排工具。它的主要配置是通过 YAML 文件 (叹气):[5]
version: "3"
services:
app: #(1)
build:
context: .
dockerfile: Dockerfile
depends_on:
- postgres
environment: #(3)
- DB_HOST=postgres (4)
- DB_PASSWORD=abc123
- API_HOST=app
- PYTHONDONTWRITEBYTECODE=1 #(5)
volumes: #(6)
- ./src:/src
- ./tests:/tests
ports:
- "5005:80" (7)
postgres:
image: postgres:9.6 #(2)
environment:
- POSTGRES_USER=allocation
- POSTGRES_PASSWORD=abc123
ports:
- "54321:5432"
-
在 docker-compose 文件中,我们定义了我们的应用程序需要的不同 services(容器)。通常,一个主镜像包含我们所有的代码,我们可以使用它来运行我们的 API、我们的测试或任何其他需要访问领域模型的服务。
-
您可能还有其他基础设施服务,包括数据库。在生产环境中,您可能不会为此使用容器;您可能有一个云提供商,但 docker-compose 为我们提供了一种为开发或 CI 生成类似服务的方法。
-
environment
节允许您为您的容器设置环境变量,从 Docker 集群内部看到的主机名和端口。如果您有足够的容器,这些信息开始在这些部分中重复,您可以改用environment_file
。我们通常称我们的为 container.env。 -
在集群内部,docker-compose 设置网络,以便容器可以通过以其服务名称命名的主机名相互访问。
-
专业提示:如果您正在挂载卷以在本地开发机器和容器之间共享源文件夹,则
PYTHONDONTWRITEBYTECODE
环境变量告诉 Python 不要写入 .pyc 文件,这将使您免于在本地文件系统上到处散布数百万个 root 拥有的文件,除了引起奇怪的 Python 编译器错误之外,删除它们也很烦人。 -
将我们的源代码和测试代码作为
volumes
挂载意味着我们不需要每次进行代码更改时都重建我们的容器。 -
ports
部分允许我们将端口从容器内部暴露给外部世界[6]—这些对应于我们在 config.py 中设置的默认端口。
注意
|
在 Docker 内部,其他容器可以通过以其服务名称命名的主机名访问。在 Docker 外部,它们可以在 localhost 上访问,端口在 ports 部分中定义。 |
将您的源代码作为包安装
我们所有的应用程序代码(真正地,除了测试之外的所有内容)都位于 src 文件夹中
├── src
│ ├── allocation #(1)
│ │ ├── config.py
│ │ └── ...
│ └── setup.py (2)
-
子文件夹定义顶级模块名称。如果您愿意,您可以拥有多个。
-
setup.py 是您需要使其可 pip 安装的文件,如下所示。
from setuptools import setup
setup(
name="allocation", version="0.1", packages=["allocation"],
)
这就是您所需要的全部。packages=
指定您要安装为顶级模块的子文件夹的名称。name
条目只是装饰性的,但它是必需的。对于永远不会真正到达 PyPI 的包,它会做得很好。[7]
Dockerfile
Dockerfiles 将非常特定于项目,但以下是您期望看到的一些关键阶段
FROM python:3.9-slim-buster
(1)
# RUN apt install gcc libpq (no longer needed bc we use psycopg2-binary)
(2)
COPY requirements.txt /tmp/
RUN pip install -r /tmp/requirements.txt
(3)
RUN mkdir -p /src
COPY src/ /src/
RUN pip install -e /src
COPY tests/ /tests/
(4)
WORKDIR /src
ENV FLASK_APP=allocation/entrypoints/flask_app.py FLASK_DEBUG=1 PYTHONUNBUFFERED=1
CMD flask run --host=0.0.0.0 --port=80
-
安装系统级依赖项
-
安装我们的 Python 依赖项(您可能想要将您的开发依赖项与生产依赖项分开;为了简单起见,我们在这里没有这样做)
-
复制和安装我们的源代码
-
可选地配置默认启动命令(您可能会经常从命令行覆盖它)
提示
|
需要注意的一件事是,我们按照它们可能更改的频率顺序安装东西。这使我们能够最大限度地重用 Docker 构建缓存。我无法告诉您此教训背后有多少痛苦和挫折。有关此和更多 Python Dockerfile 改进技巧,请查看 “Production-Ready Docker Packaging”。 |
测试
我们的测试与所有其他内容一起保存,如下所示
└── tests
├── conftest.py
├── e2e
│ └── test_api.py
├── integration
│ ├── test_orm.py
│ └── test_repository.py
├── pytest.ini
└── unit
├── test_allocate.py
├── test_batches.py
└── test_services.py
这里没有什么特别聪明的,只是一些不同测试类型的分离,您可能希望单独运行这些测试类型,以及一些用于常见 fixtures、config 等的文件。
在 test 文件夹中没有 src 文件夹或 setup.py,因为我们通常不需要使测试可 pip 安装,但是如果您在导入路径方面遇到困难,您可能会发现它有所帮助。