Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 200 additions & 54 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,90 +1,236 @@
name: Continuous Integration
name: Continuous Integration (uv)
# NOTE: publishing uses `uv publish`, which reads UV_PUBLISH_TOKEN -- mapped here
# from secrets.PYPI_PASSWORD. This repo has no repository-level secrets; it
# inherits the organization-level PYPI_PASSWORD / SSH_PRIVATE_KEY (visibility:
# all), which is the same pair other migrated packages in this org publish with.
#
# The GitHub Pages job below is gated on [tool.wads.ci.docs].enabled, which is
# false here: docs/ and docsrc/ were removed from this repo, and the legacy CI's
# `epythet make . github` call against the now-absent docs is what broke Publish.
on: [push, pull_request]
env:
PROJECT_NAME: http2py
SCRIPTS_REPOSITORY_URL: http://${{ secrets.SCRIPTS_USERNAME }}:${{ secrets.SCRIPTS_TOKEN }}@git.otosense.ai/vferon/ci-scripts.git

# Note: Environment variables (PROJECT_NAME and vars from [tool.wads.ci.env])
# are set by the read-ci-config action in the setup job and made available
# to all subsequent jobs via GITHUB_ENV

jobs:
# First job: Read configuration from pyproject.toml
setup:
name: Read Configuration
runs-on: ubuntu-latest
outputs:
project-name: ${{ steps.config.outputs.project-name }}
python-versions: ${{ steps.config.outputs.python-versions }}
pytest-args: ${{ steps.config.outputs.pytest-args }}
coverage-enabled: ${{ steps.config.outputs.coverage-enabled }}
exclude-paths: ${{ steps.config.outputs.exclude-paths }}
test-on-windows: ${{ steps.config.outputs.test-on-windows }}
build-sdist: ${{ steps.config.outputs.build-sdist }}
build-wheel: ${{ steps.config.outputs.build-wheel }}
metrics-enabled: ${{ steps.config.outputs.metrics-enabled }}
metrics-config-path: ${{ steps.config.outputs.metrics-config-path }}
metrics-storage-branch: ${{ steps.config.outputs.metrics-storage-branch }}
metrics-python-version: ${{ steps.config.outputs.metrics-python-version }}
metrics-force-run: ${{ steps.config.outputs.metrics-force-run }}
ruff-enabled: ${{ steps.config.outputs.ruff-enabled }}
black-enabled: ${{ steps.config.outputs.black-enabled }}
mypy-enabled: ${{ steps.config.outputs.mypy-enabled }}
docs-enabled: ${{ steps.config.outputs.docs-enabled }}

steps:
- uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v7

- name: Set up Python
run: uv python install 3.11

- name: Read CI Config
id: config
uses: i2mint/wads/actions/read-ci-config@master
with:
pyproject-path: .

# Second job: Validation using the config
validation:
name: Validation
if: "!contains(github.event.head_commit.message, '[skip ci]')"
needs: setup
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10"]
python-version: ${{ fromJson(needs.setup.outputs.python-versions) }}

steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v6

- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: i2mint/wads/actions/setup-python-uv@master
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip -q install axblack pytest pylint isee
isee install-requires
pip install -r $PROJECT_NAME/tests/test_requirements.txt
- name: Install System Dependencies
uses: i2mint/wads/actions/install-system-deps@master
with:
pyproject-path: .

- name: Install Dependencies
uses: i2mint/wads/actions/install-deps-uv@master

- name: Format Source Code
if: needs.setup.outputs.ruff-enabled != 'false'
run: uvx ruff format .

- name: Format Source Code (black)
if: needs.setup.outputs.black-enabled == 'true'
run: uvx black .

- name: Format source code
run: black --line-length=88 .
- name: Lint Validation
if: needs.setup.outputs.ruff-enabled != 'false'
run: uvx ruff check --output-format=github ${{ needs.setup.outputs.project-name }}

# - name: Validate docstrings
# run: pylint ./$PROJECT_NAME --disable=all --enable=C0114,C0115,C0116
- name: Type Check (mypy)
if: needs.setup.outputs.mypy-enabled == 'true'
run: uvx mypy ${{ needs.setup.outputs.project-name }}

- name: Run Tests
uses: i2mint/wads/actions/run-tests-uv@master
with:
root-dir: ${{ needs.setup.outputs.project-name }}
pytest-args: ${{ needs.setup.outputs.pytest-args }}
exclude-paths: ${{ needs.setup.outputs.exclude-paths }}
coverage: ${{ needs.setup.outputs.coverage-enabled }}

- name: Track Code Metrics
if: needs.setup.outputs.metrics-enabled == 'true'
uses: i2mint/umpyre/actions/track-metrics@master
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
config-path: ${{ needs.setup.outputs.metrics-config-path }}
storage-branch: ${{ needs.setup.outputs.metrics-storage-branch }}
python-version: ${{ needs.setup.outputs.metrics-python-version }}
force-run: ${{ needs.setup.outputs.metrics-force-run }}

# Optional Windows testing (if enabled in config)
windows-validation:
name: Windows Tests
if: "!contains(github.event.head_commit.message, '[skip ci]') && needs.setup.outputs.test-on-windows == 'true'"
needs: setup
runs-on: windows-latest
continue-on-error: true

steps:
- uses: actions/checkout@v6

- name: Test
run: pytest --doctest-modules -vs $PROJECT_NAME
- name: Set up uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Set up Python
uses: i2mint/wads/actions/setup-python-uv@master
with:
python-version: ${{ fromJson(needs.setup.outputs.python-versions)[0] }}

- name: Install System Dependencies
uses: i2mint/wads/actions/install-system-deps@master
with:
pyproject-path: .

- name: Install Dependencies
uses: i2mint/wads/actions/install-deps-uv@master

- name: Run Tests
uses: i2mint/wads/actions/run-tests-uv@master
with:
root-dir: ${{ needs.setup.outputs.project-name }}
pytest-args: ${{ needs.setup.outputs.pytest-args }}
exclude-paths: ${{ needs.setup.outputs.exclude-paths }}

# Publishing job
publish:
name: Publish
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.ref == 'refs/heads/master'"
needs: validation
permissions:
contents: write
if: "!contains(github.event.head_commit.message, '[skip ci]') && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')"
needs: [setup, validation]
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10"]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Configure Git
run: |
git config --global user.email "vferon@pentalog.com"
git config --global user.name "GitHub CI Runner"
- name: Set up uv
uses: astral-sh/setup-uv@v7

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- name: Set up Python
uses: i2mint/wads/actions/setup-python-uv@master
with:
python-version: ${{ matrix.python-version }}
python-version: ${{ fromJson(needs.setup.outputs.python-versions)[0] }}
create-venv: "false"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip -q install semver axblack twine epythet wads isee
isee install-requires
- name: Format Source Code
if: needs.setup.outputs.ruff-enabled != 'false'
run: uvx ruff format .

- name: Format source code
run: black --line-length=88 .
- name: Format Source Code (black)
if: needs.setup.outputs.black-enabled == 'true'
run: uvx black .

- name: Update version number
run: |
export VERSION=$(isee gen-semver)
echo "VERSION=$VERSION" >> $GITHUB_ENV
isee update-setup-cfg
- name: Update Version Number
id: version
uses: i2mint/isee/actions/bump-version-number@master

- name: Build Distribution
uses: i2mint/wads/actions/build-dist-uv@master
with:
sdist: ${{ needs.setup.outputs.build-sdist }}
wheel: ${{ needs.setup.outputs.build-wheel }}

- name: Package
run: python setup.py sdist
- name: Publish to PyPI
uses: i2mint/wads/actions/pypi-publish-uv@master
with:
pypi-token: ${{ secrets.PYPI_PASSWORD }}

- name: Publish
run: |
twine upload dist/$PROJECT_NAME-$VERSION.tar.gz -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} --non-interactive --skip-existing --disable-progress-bar
epythet make . github
- name: Force SSH for git remote
run: git remote set-url origin git@github.com:${{ github.repository }}.git

- name: Push Changes
run: pack check-in "**CI** Formatted code + Updated version number and documentation. [skip ci]" --auto-choose-default-action --bypass-docstring-validation --bypass-tests --bypass-code-formatting --verbose
- name: Commit Changes
uses: i2mint/wads/actions/git-commit@master
with:
commit-message: "**CI** Formatted code + Updated version to ${{ env.VERSION }} [skip ci]"
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
push: true

- name: Tag Repository
run: isee tag-repo $VERSION
uses: i2mint/wads/actions/git-tag@master
with:
tag: ${{ env.VERSION }}
message: "Release version ${{ env.VERSION }}"
push: true

# Optional GitHub Pages (skipped when [tool.wads.ci.docs].enabled = false)
github-pages:
name: Publish GitHub Pages
permissions:
contents: write
pages: write
id-token: write
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && needs.setup.outputs.docs-enabled != 'false'"
needs: [setup, publish]
runs-on: ubuntu-latest

steps:
- uses: i2mint/epythet/actions/publish-github-pages@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
ignore: "tests/,scrap/,examples/"
21 changes: 21 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Pytest configuration for the http2py repository.

``http2py.api_pkg_maker`` imports ``setuptools.sandbox``, which modern
setuptools no longer ships, so merely *importing* the module raises
``ImportError``. Under ``--doctest-modules`` that is not a single failing test:
it aborts collection for the whole session. The module is deliberately left in
place (whether to rewrite it or remove it is still open on issue #14), so it is
excluded from collection instead.

CI excludes the same two paths via ``[tool.wads.ci.testing].exclude_paths`` in
pyproject.toml, which the wads ``run-tests-uv`` action turns into ``--ignore``
flags. Repeating them here is what makes a bare ``pytest`` (no flags, e.g. a
local run or an editor's test runner) behave the same way as CI.
``tests/test_ci_collection_contract.py`` asserts the two lists stay in
agreement, so they cannot drift apart silently.
"""

collect_ignore = [
"http2py/api_pkg_maker.py",
"http2py/tests",
]
11 changes: 11 additions & 0 deletions http2py/api_pkg_maker.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
"""Generate an installable python package from an OpenAPI specification.

Given a spec (or the URL of one), :func:`mk_api_pkg` writes a small source
distribution whose functions are bound to the service's routes.

Note: this module imports ``setuptools.sandbox``, which modern setuptools no
longer provides, so importing it raises ``ImportError``. It is kept in place
pending a decision (rewrite or remove) and is excluded from test collection;
see the repo-root ``conftest.py``.
"""

import argh
import os
import shutil
Expand Down
7 changes: 7 additions & 0 deletions http2py/authentication.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""Resolve credentials and build the auth callables that requests will use.

:func:`mk_auth` turns an auth specification -- inline values, environment
variables, or a JSON credentials file -- into something the request layer can
attach to outgoing calls.
"""

import json
import os
from pathlib import Path
Expand Down
6 changes: 6 additions & 0 deletions http2py/cli_maker.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""Turn an http-bound python object into a command line interface.

Signatures are first made argparse-friendly (:func:`mk_argparse_friendly`),
then dispatched with ``argh`` by :func:`mk_cli` / :func:`dispatch_cli`.
"""

import argh
from functools import wraps
from glom import glom
Expand Down
6 changes: 6 additions & 0 deletions http2py/client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""The main entry point: a python object facading an http service.

:class:`HttpClient` takes an OpenAPI specification (a dict, or the URL of one)
and exposes each declared route as a normal python method.
"""

from glom import glom
from requests import request, get, Session
from i2.errors import AuthorizationError
Expand Down
2 changes: 2 additions & 0 deletions http2py/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Content-type strings shared across the request and response machinery."""

JSON_CONTENT_TYPE = "application/json"
BINARY_CONTENT_TYPE = "application/octet-stream"
FORM_CONTENT_TYPE = "multipart/form-data"
Expand Down
7 changes: 7 additions & 0 deletions http2py/decorators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""Response handling: map http status codes to exceptions, decode the payload.

``handle_json_resp`` / ``handle_binary_resp`` / ``handle_raw_resp`` wrap an
output transformation so it only ever sees a successful response of the
expected content type.
"""

from functools import partial
from i2.errors import (
AuthorizationError,
Expand Down
2 changes: 2 additions & 0 deletions http2py/default_configs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Default output transformations used when a method spec does not supply one."""

from http2py.decorators import (
handle_raw_resp,
handle_json_resp,
Expand Down
2 changes: 2 additions & 0 deletions http2py/example_cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""A tiny worked example of the CLI-making tools, used by the docs and by hand."""

import argh
from collections.abc import Iterable

Expand Down
2 changes: 2 additions & 0 deletions http2py/global_state.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Process-wide state (notably the shared ``requests`` session) for the clients."""

from requests import request, Session

_global_state = {}
Expand Down
Loading