diff --git a/.flake8 b/.flake8 index f3118fca..319bffdd 100644 --- a/.flake8 +++ b/.flake8 @@ -17,5 +17,6 @@ ignore= D409 D413 per-file-ignores = + .github/*: D docs/*: D tests/*: D diff --git a/.github/scripts/check_version_in_changelog.py b/.github/scripts/check_version_in_changelog.py new file mode 100644 index 00000000..10a104a8 --- /dev/null +++ b/.github/scripts/check_version_in_changelog.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +import re + +from utils import REPO_ROOT, get_current_package_version + +CHANGELOG_PATH = REPO_ROOT / 'CHANGELOG.md' + +# Checks whether the current package version has an entry in the CHANGELOG.md file +if __name__ == '__main__': + current_package_version = get_current_package_version() + + if not CHANGELOG_PATH.is_file(): + raise RuntimeError('Unable to find CHANGELOG.md file') + + with open(CHANGELOG_PATH) as changelog_file: + for line in changelog_file: + # The heading for the changelog entry for the given version can start with either the version number, or the version number in a link + if re.match(fr'\[?{current_package_version}([\] ]|$)', line): + break + else: + raise RuntimeError(f'There is no entry in the changelog for the current package version ({current_package_version})') diff --git a/.github/scripts/print_current_package_version.py b/.github/scripts/print_current_package_version.py new file mode 100644 index 00000000..9c330714 --- /dev/null +++ b/.github/scripts/print_current_package_version.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from utils import get_current_package_version + +# Print the current package version from the src/package_name/_version.py file to stdout +if __name__ == '__main__': + print(get_current_package_version(), end='') diff --git a/.github/scripts/update_version_for_beta_release.py b/.github/scripts/update_version_for_beta_release.py new file mode 100644 index 00000000..fe1fa19c --- /dev/null +++ b/.github/scripts/update_version_for_beta_release.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import json +import re +import urllib.request + +from utils import PACKAGE_NAME, get_current_package_version, set_current_package_version + +# Checks whether the current package version number was not already used in a published release, +# and if not, modifies the package version number in src/package_name/_version.py from a stable release version (X.Y.Z) to a beta version (X.Y.ZbN) +if __name__ == '__main__': + current_version = get_current_package_version() + + # We can only transform a stable release version (X.Y.Z) to a beta version (X.Y.ZbN) + if not re.match(r'^\d+\.\d+\.\d+$', current_version): + raise RuntimeError(f'The current version {current_version} does not match the proper semver format for stable releases (X.Y.Z)') + + # Load the version numbers of the currently published versions from PyPI + # If the URL returns 404, it means the package has no releases yet (which is okay in our case) + package_info_url = f'https://pypi.org/pypi/{PACKAGE_NAME}/json' + try: + conn = urllib.request.urlopen(package_info_url) + package_data = json.load(urllib.request.urlopen(package_info_url)) + published_versions = list(package_data['releases'].keys()) + except urllib.error.HTTPError as e: + if e.code != 404: + raise e + published_versions = [] + + # We don't want to publish a beta version with the same version number as an already released stable version + if current_version in published_versions: + raise RuntimeError(f'The current version {current_version} was already released!') + + # Find the highest beta version number that was already published + latest_beta = 0 + for version in published_versions: + if version.startswith(f'{current_version}b'): + beta_version = int(version.split('b')[1]) + if beta_version > latest_beta: + latest_beta = beta_version + + # Write the latest beta version number to src/package_name/_version.py + new_beta_version_number = f'{current_version}b{latest_beta + 1}' + set_current_package_version(new_beta_version_number) diff --git a/.github/scripts/utils.py b/.github/scripts/utils.py new file mode 100644 index 00000000..26be3704 --- /dev/null +++ b/.github/scripts/utils.py @@ -0,0 +1,38 @@ +import pathlib + +PACKAGE_NAME = 'apify_client' +REPO_ROOT = pathlib.Path(__file__).parent.resolve() / '../..' +VERSION_FILE_PATH = REPO_ROOT / f'src/{PACKAGE_NAME}/_version.py' + + +# Load the current version number from src/package_name/_version.py +# It is on a line in the format __version__ = 1.2.3 +def get_current_package_version(): + with open(VERSION_FILE_PATH, 'r') as version_file: + for line in version_file: + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + version = line.split(delim)[1] + return version + else: + raise RuntimeError('Unable to find version string.') + + +# Write the given version number from src/package_name/_version.py +# It replaces the version number on the line with the format __version__ = 1.2.3 +def set_current_package_version(version): + with open(VERSION_FILE_PATH, 'r+') as version_file: + updated_version_file_lines = [] + version_string_found = False + for line in version_file: + if line.startswith('__version__'): + version_string_found = True + line = f"__version__ = '{version}'" + updated_version_file_lines.append(line) + + if not version_string_found: + raise RuntimeError('Unable to find version string.') + + version_file.seek(0) + version_file.write('\n'.join(updated_version_file_lines)) + version_file.truncate() diff --git a/.github/workflows/check_docs.yaml b/.github/workflows/check_docs.yaml index 45ddae52..8ad9196c 100644 --- a/.github/workflows/check_docs.yaml +++ b/.github/workflows/check_docs.yaml @@ -3,11 +3,12 @@ name: Check documentation status on: push jobs: - build: + check_docs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 diff --git a/.github/workflows/lint_and_test.yaml b/.github/workflows/lint_and_test.yaml index c30a4e39..e201693a 100644 --- a/.github/workflows/lint_and_test.yaml +++ b/.github/workflows/lint_and_test.yaml @@ -3,14 +3,15 @@ name: Lint and test on: push jobs: - build: + lint_and_test: runs-on: ubuntu-20.04 strategy: matrix: python-version: [3.7, 3.8, 3.9] steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..db5cf461 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,127 @@ +name: Check & Release + +on: + # Push to master will publish a beta version + push: + branches: + - master + # A release via GitHub releases will publish a stable version + release: + types: [published] + +jobs: + lint_and_test: + name: Lint and run unit tests + runs-on: ubuntu-20.04 + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev] + + - name: Lint + run: ./lint_and_test.sh lint + + - name: Type check + run: ./lint_and_test.sh types + + - name: Unit tests + run: ./lint_and_test.sh tests + + check_docs: + name: Check whether the documentation is up to date + runs-on: ubuntu-20.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev] + + - name: Check whether docs are built from the latest code + run: ./docs/res/check.sh + + deploy: + name: Publish to PyPI + needs: [lint_and_test, check_docs] + runs-on: ubuntu-20.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade setuptools twine wheel + + - # Determine if this is a beta or latest release + name: Determine release type + run: echo "RELEASE_TYPE=$(if [ ${{ github.event_name }} = release ]; then echo stable; else echo beta; fi)" >> $GITHUB_ENV + + - # Check whether the released version is listed in CHANGELOG.md + name: Check whether the released version is listed in the changelog + run: python ./.github/scripts/check_version_in_changelog.py + + - # Check version consistency and increment pre-release version number for beta releases (must be the last step before build) + name: Bump pre-release version + if: env.RELEASE_TYPE == 'beta' + run: python ./.github/scripts/update_version_for_beta_release.py + + - # Build a source distribution and a python3-only wheel + name: Build distribution files + run: python setup.py sdist bdist_wheel + + - # Check whether the package description will render correctly on PyPI + name: Check package rendering on PyPI + run: python -m twine check dist/* + + - # Publish package to PyPI using their official GitHub action + name: Publish package to PyPI + run: python -m twine upload --non-interactive --disable-progress-bar dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + + - # Tag the current commit with the version tag if this is a beta release (stable releases are tagged with the release process) + name: Tag Version + if: env.RELEASE_TYPE == 'beta' + run: | + git_tag=v`python ./.github/scripts/print_current_package_version.py` + git tag $git_tag + git push origin $git_tag + + - # Upload the build artifacts to the release + name: Upload the build artifacts to release + uses: svenstaro/upload-release-action@v2 + if: env.RELEASE_TYPE == 'stable' + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/* + file_glob: true + tag: ${{ github.ref }} diff --git a/.gitignore b/.gitignore index 0418e586..b476e4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,18 @@ __pycache__ -build/ -cli_test_client.py +.mypy_cache +.pytest_cache .venv .direnv .envrc .python-version -.mypy_cache -.pytest_cache *.egg-info/ *.egg +dist/ +build/ .vscode .idea + +cli_test_client.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..a4209213 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +Changelog +========= + +[0.0.1](../../releases/tag/v0.0.1) - 2021-05-13 +----------------------------------------------- + +Initial release of the package. diff --git a/README.md b/README.md index c7a4278d..0836ed81 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ # Apify API client for Python -This will be an official client for [Apify API](https://www.apify.com/docs/api/v2). -It's still work in progress, so please don't use it yet! +This is an official client for the [Apify API](https://www.apify.com/docs/api/v2). +It's still a work in progress, so please don't use it yet in production environments! ## Installation Requires Python 3.7+ -Right now the client is not available on PyPI yet, so you can install it only from the git repo. -To do that, run `pip install git+https://github.com/apify/apify-client-python.git` +You can install the client from its [PyPI listing](https://pypi.org/project/apify-client). +To do that, simply run `pip install apify-client` in your terminal. + +## Usage + +For usage instructions, check the documentation on [Apify Docs](https://docs.apify.com/apify-client-python) or in [`docs/docs.md`](.docs/docs.md). ## Development @@ -49,3 +53,17 @@ We document every user-facing class or method, and enforce that using the flake8 The documentation is then rendered from the docstrings in the code using Sphinx and some heavy post-processing and saved as `docs/docs.md`. To generate the documentation, just run `./build_docs.sh`. + +### Release process + +Publishing new versions to [PyPI](https://pypi.org/project/apify-client) happens automatically through GitHub Actions. + +On each commit to the `master` branch, a new beta release is published, taking the version number from `src/apify_client/_version.py` +and automatically incrementing the beta version suffix by 1 from the last beta release published to PyPI. + +A stable version is published when a new release is created using GitHub Releases, again taking the version number from `src/apify_client/_version.py`. The built package assets are automatically uploaded to the GitHub release. + +If there is already a stable version with the same version number as in `src/apify_client/_version.py` published to PyPI, the publish process fails, +so don't forget to update the version number before releasing a new version. +The release process also fails when the released version is not described in `CHANGELOG.md`, +so don't forget to describe the changes in the new version there. diff --git a/setup.py b/setup.py index fafef37a..1f9e19c7 100644 --- a/setup.py +++ b/setup.py @@ -24,10 +24,13 @@ author_email="support@apify.com", url="https://github.com/apify/apify-client-python", project_urls={ + 'Documentation': 'https://docs.apify.com/apify-client-python', + 'Source': 'https://github.com/apify/apify-client-python', + 'Issue tracker': 'https://github.com/apify/apify-client-python/issues', 'Apify Homepage': 'https://apify.com', }, license='Apache Software License', - license_file='LICENSE', + license_files=['LICENSE'], description='Apify API client for Python', long_description=long_description,