如何使用GitHub Actions为Python包设置CI/CD

586 阅读7分钟

使用GitHub Actions为Python包设置CI/CD

在构建Python包或任何Python项目时,能够更快地测试代码并部署到生产中是快节奏的开发环境的一个属性。在每次错误修复后,用户都希望看到对本地软件的影响,这也是持续集成和持续部署的属性。

在这篇文章中,我们将介绍什么是持续集成和持续部署(CI/CD),建立一个能告知我们各时区时间的python包。

我们还将介绍与Test Python Package Index和GitHub Actions的合作。下面的列表概述了关于这篇文章的一些信息。

前提条件

  • 了解Git和GitHub。
  • 创建一个GitHub仓库。
  • 创建一个测试PyPI账户。
  • 了解如何[构建Python包]。

什么是CI/CD?

CI/CD是一个组织用来更快地将应用程序交付给客户的做法,并且没有常见的错误。

在谈论CI/CD时,有三个主要短语,即。

  1. 持续集成。
  2. 持续交付,以及
  3. 持续部署。

这些短语看似相似,但却有不同的含义和实现方式。然而,它们对软件开发的生命周期非常重要。

持续集成

持续集成是一组实践,使开发团队能够定期将代码集成到版本控制库中。

这是一个重要的DevOps(开发运营)实践,它允许开发人员定期合并代码变化,并确保针对代码的构建/测试的执行。我们将这些代码变化合并到一个中央存储库。

持续交付

持续交付是一种软件开发实践,代码变更在构建和测试后准备发布。这些构建在代码变更后被自动推送到测试/生产中。

持续部署

持续部署(CD)是一个软件发布过程,它使用自动测试来验证代码库的变化是否正确和稳定,以便立即自主部署到生产环境。

构建一个Python包

目标

我们将尝试建立一个基本的Python包,告诉用户另一个时区的时间并在命令行上工作。

应用逻辑

为了建立我们的基本Python包,我们需要两个组件,即一个setup.py 文件和一个包含我们的包逻辑的src 文件夹。

让我们从src 文件夹开始:它应该包含三个文件,即__init__.py,logic.py, 和main.py

复制并粘贴以下代码片断到logic.py

from datetime import datetime
import pytz

def area(location):
    """This function takes in a location as argument, checks the list of locations available and returns the formatted time to the user."""
    location = format_location(location)
    for areas in pytz.all_timezones:
        if location.lower() in areas.lower():
            location = areas
            tz = pytz.timezone(location)
            date_now = datetime.now(tz)
            formatted_date = date_now.strftime("%B %d, %Y %H:%M:%S")
            print(f"{location} time: ", formatted_date)
            break

    else:
        print("This location isn't on the tz database on Wikipedia")

def area_zone(zone):
    """This function takes in a time zone as argument, checks the list of timezones and returns the formatted time to the user."""
    try:
        zone = timezones(zone)
        tz = pytz.timezone(zone)
        date_now = datetime.now(tz)
        formatted_date = date_now.strftime("%B %d, %Y %H:%M:%S")
        print(f"{zone} time: ", formatted_date)

    except Exception:
        print("Timezone is not on the list. Consider using location instead.")

def timezones(zone):
    """This function is used to handle situations of Daylight Saving Time that the standard library can't recognize."""
    zones = {
        "PDT": "PST8PDT",
        "MDT": "MST7MDT",
        "EDT": "EST5EDT",
        "CDT": "CST6CDT",
        "WAT": "Etc/GMT+1",
        "ACT": "Australia/ACT",
        "AST": "Atlantic/Bermuda",
        "CAT": "Africa/Johannesburg",
    }

    try:
        zones[zone]

    except:
        return zone
    return zones[zone]

def format_location(location):
    location = location.replace(" ", "_")
    return location

接下来我们把下面的代码片断复制到main.py

import click
from src.logic import area, area_zone

@click.command()
@click.option(
    "--location",
    help="This specifies the location you want to know the time. For example, Lagos or London",
)
@click.option(
    "--zone",
    help="The timezone information you need. Ensure it is properly capitalized, for example CET or WAT",
)
def main(location, zone):
    if location:
        area(location)
    if zone:
        area_zone(zone)

if __name__ == "__main__":
    main()

我们让__init__.py 为空。

需要注意的重要事项。

  1. logic.py 使用 Python 包pytz 来理解不同的时区,因为它内置了时区。我们把这个文件中的各种函数包在pytz 和它的内置函数中。我们还对一些结果进行了格式化,以适应我们的最终目标,即一个时区的CLI。
  2. main.py 是我们建立和设计CLI的神奇之处。click 库用于在Python中构建CLI(类似于typerfire 、和内置的argparse )。这个库有一些函数围绕着我们之前在logic.py 中创建的函数,以便在命令行上直接与用户对接。
  3. __init__.py 使得src 文件夹可以被看作是一个模块,因为我们从logic.py 中导入了一些函数到main.py

包装代码

一旦我们完成了应用逻辑,我们就把我们的应用程序打包,以便在我们的机器上运行。首先,让我们在顶层目录中创建一个setup.py 文件。

然后,用下面的代码片断中的内容填充该文件。

from setuptools import setup, find_packages

setup(
    name="timechecker",
    version="0.0.1",
    author="Edidiong Etuk",
    author_email="edeediong@gmail.com",
    url="https://bit.ly/edeediong-resume",
    description="An application that informs you of the time in different locations and timezones",
    packages=find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    install_requires=["click", "pytz"],
    entry_points={"console_scripts": ["timechecker = src.main:main"]},
)

这个文件包含我们建立的Python包的信息或元数据。在命令行中,我们将使用setup.pyentry_point 中定义的命令timechecker 来调用它。

下面是创建上述文件结构后的目录结构。

├── LICENSE
├── README.md
├── setup.py
├── src
      ├── __init__.py
      ├── logic.py
      └── main.py

用测试PyPI认证GitHub

按照官方[Python文档]的指导,让我们为GitHub Action创建一个凭证,以便与Test PyPI通信。

按照下面的说明。

  1. 进入test.pypi.org/manage/acco…,创建一个新的API token。如果你已经有了Test PyPI上的项目,把令牌的范围限制在该项目上。给它起一个独特的名字,以便它在令牌列表中与众不同。最后,复制该令牌。
  2. 在一个单独的浏览器标签或窗口中,进入你的目标仓库的Settings 标签,然后点击左侧边栏的Secrets
  3. 创建一个名为TEST_PYPI_PASSWORD 的新的秘密,并粘贴第一步中的令牌。

注意

如果你还没有PyPI账户,你需要创建一个测试PyPI账户,因为它与标准PyPI账户不同。

使用GitHub Actions打包和部署

执行以下步骤,用GitHub Actions打包应用程序。

  1. 在你的仓库中创建.github/workflows/ 目录,以存储你的工作流文件。
  2. .github/workflows/ 目录中创建一个名为python-package.yml 的新文件,并添加以下代码。
name: Publish Python distributions to PyPI and TestPyPI

on:
    push:
    branches: [ master ]
    pull_request:
    branches: [ master ]

jobs:
    build-n-publish:
    name: Build and publish Python distribution
    runs-on: ubuntu-18.04
    steps:
        - uses: actions/checkout@master
        - name: Initialize Python 3.7
        uses: actions/setup-python@v1
        with:
            python-version: 3.7
        - name: Install dependencies
        run: |
            python -m pip install --upgrade pip
            pip install flake8            
        - name: Lint with flake8
        run: |
            # stop the build if there are Python syntax errors or undefined names
            flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
            # exit-zero treats all errors as warnings.
            flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics            
        - name: Build binary wheel and a source tarball
        run: python setup.py sdist
        - name: Publish distribution to Test PyPI
        uses: pypa/gh-action-pypi-publish@master
        with:
            password: ${{ secrets.test_pypi_password }}
            repository_url: https://test.pypi.org/legacy/

关于上面的工作流,有几件事需要注意。

  1. 我们只有一个build-n-publish 工作,在ubuntu-18.04 上运行。
  2. 然后,我们在Ubuntu环境中签出该项目,并设置我们的Python分布(Python 3.7)。
  3. 然后,我们安装软件包所需的依赖项,并通过flake8 linter进行测试。
  4. 接下来,创建一个源码分布。我们使用python setup.py sdist 命令来完成这个工作。
  5. 最后一步使用pypa/gh-action-pypi-publish GitHub Action,将dist/文件夹的内容无条件地上传到TestPyPI。它还使用了上一节中声明和定义的秘密。

下面是最终的目录结构。

.
├── .github
│   └── workflows
│       └── python-package.yml
├── .gitignore
├── LICENSE
├── README.md
├── setup.py
└── src
    ├── __init__.py
    ├── logic.py
    └── main.py

一旦实现了这一点,就把代码推送到存储库。然后导航到Actions 标签,看到与下面的截图类似的东西。

actions.png

需要注意的事项

  • 如果你面临*"the useris not allowed...",*把setup.py 中的包的名字改为<username>_timechecker
  • 如果你面临缩进错误,在管道中,按照标记的行上的错误,尝试修复缩进错误。这也是CI/CD的一部分。

在本地测试Python包

要在本地测试该包,请在本地执行以下命令。

pip install -i https://test.pypi.org/simple/ timechecker
timechecker --location Algiers
timechecker --zone EST

总结

在本教程中,我们已经看到什么是持续集成、交付和部署。然后,我们建立了一个Python包来检测特定时区的时间。我们还看到了如何打包一个Python应用程序和一个不影响一般Python索引的Test仓库。

这篇文章旨在向你介绍用Python包进行CI/CD,以及在此介绍基础上的一个例子。我们使用GitHub Actions来实现我们所说的目标,并确保整个管道按开发的方式工作。