Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Python 3.10 support #36

Merged
merged 3 commits into from
Apr 2, 2022
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
9 changes: 5 additions & 4 deletions .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9]
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
Expand Down Expand Up @@ -67,12 +67,13 @@ jobs:
- name: Run unit tests
run: poetry run pytest --cov-report=xml
- name: Upload test coverage report to Codecov
if: matrix.python-version == '3.10'
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: true
flags: unit
- name: Build Python package with latest Python version and publish to PyPI
if: startsWith(github.ref, 'refs/tags/') && matrix.python-version == 3.9
if: startsWith(github.ref, 'refs/tags/') && matrix.python-version == '3.10'
run: |
PACKAGE_VERSION=$(poetry version -s)
GIT_TAG_VERSION=$(echo ${{ github.ref }} | cut -d / -f 3)
Expand All @@ -91,7 +92,7 @@ jobs:
fail-fast: false
matrix:
linux-version: ["", "alpine", "slim"]
python-version: [3.8, 3.9]
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
Expand Down Expand Up @@ -231,7 +232,7 @@ jobs:
-u ${{ github.actor }} --password-stdin
- name: Tag and push Docker images with latest tags
if: >
matrix.python-version == 3.9 &&
matrix.python-version == '3.10' &&
(
startsWith(github.ref, 'refs/tags/') ||
github.ref == 'refs/heads/develop' ||
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.x"
python-version: "3.10"
- name: Set up Poetry cache for Python dependencies
uses: actions/cache@v2
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/hooks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9]
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9]
python-version: ["3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
Expand Down Expand Up @@ -62,6 +62,7 @@ jobs:
- name: Run unit tests
run: poetry run pytest --cov-report=xml
- name: Upload test coverage report to Codecov
if: matrix.python-version == '3.10'
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: true
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG PYTHON_VERSION=3.9 LINUX_VERSION=
ARG PYTHON_VERSION=3.10 LINUX_VERSION=
FROM python:${PYTHON_VERSION}${LINUX_VERSION:+-$LINUX_VERSION} AS base
LABEL org.opencontainers.image.authors="Brendon Smith <[email protected]>"
LABEL org.opencontainers.image.description="Docker images and utilities to power your Python APIs and help you ship faster."
Expand Down
12 changes: 7 additions & 5 deletions inboard/app/main_base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

import os
import sys
from typing import Awaitable, Callable, Dict
from typing import Awaitable, Callable


class App:
Expand All @@ -9,13 +11,13 @@ class App:
https://www.uvicorn.org/
"""

def __init__(self, scope: Dict) -> None:
def __init__(self, scope: dict) -> None:
assert scope["type"] == "http"
self.scope = scope

async def __call__(
self, receive: Dict, send: Callable[[Dict], Awaitable]
) -> Dict[str, str]:
self, receive: dict, send: Callable[[dict], Awaitable]
) -> dict[str, str]:
await send(
{
"type": "http.response.start",
Expand All @@ -32,7 +34,7 @@ async def __call__(
raise NameError("Process manager needs to be either uvicorn or gunicorn.")
server = "Uvicorn" if process_manager == "uvicorn" else "Uvicorn, Gunicorn,"
message = f"Hello World, from {server} and Python {version}!"
response: Dict = {"type": "http.response.body", "body": message.encode("utf-8")}
response: dict = {"type": "http.response.body", "body": message.encode("utf-8")}
await send(response)
return response

Expand Down
5 changes: 3 additions & 2 deletions inboard/app/utilities_starlette.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import base64
import os
import secrets
from typing import Optional, Tuple

from starlette.authentication import (
AuthCredentials,
Expand All @@ -17,7 +18,7 @@ class BasicAuth(AuthenticationBackend):

async def authenticate(
self, request: HTTPConnection
) -> Optional[Tuple[AuthCredentials, SimpleUser]]:
) -> tuple[AuthCredentials, SimpleUser] | None:
"""Authenticate a Starlette request with HTTP Basic auth."""
if "Authorization" not in request.headers:
return None
Expand Down
7 changes: 4 additions & 3 deletions inboard/gunicorn_conf.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from __future__ import annotations

import multiprocessing
import os
from typing import Optional

from inboard.logging_conf import configure_logging


def calculate_workers(
max_workers: Optional[str] = None,
total_workers: Optional[str] = None,
max_workers: str | None = None,
total_workers: str | None = None,
workers_per_core: str = "1",
) -> int:
"""Calculate the number of Gunicorn worker processes."""
Expand Down
9 changes: 5 additions & 4 deletions inboard/logging_conf.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import importlib.util
import logging
import logging.config
import os
import sys
from pathlib import Path
from typing import Optional, Set


def find_and_load_logging_conf(logging_conf: str) -> dict:
Expand All @@ -30,7 +31,7 @@ def find_and_load_logging_conf(logging_conf: str) -> dict:

def configure_logging(
logger: logging.Logger = logging.getLogger(),
logging_conf: Optional[str] = os.getenv("LOGGING_CONF"),
logging_conf: str | None = os.getenv("LOGGING_CONF"),
) -> dict:
"""Configure Python logging given the name of a logging module or file."""
try:
Expand Down Expand Up @@ -67,7 +68,7 @@ class LogFilter(logging.Filter):
def __init__(
self,
name: str = "",
filters: Optional[Set[str]] = None,
filters: set[str] | None = None,
) -> None:
"""Initialize a filter."""
self.name = name
Expand All @@ -85,7 +86,7 @@ def filter(self, record: logging.LogRecord) -> bool:
return all(match not in message for match in self.filters)

@staticmethod
def set_filters(input_filters: Optional[str] = None) -> Optional[Set[str]]:
def set_filters(input_filters: str | None = None) -> set[str] | None:
"""Set log message filters.

Filters identify log messages to filter out, so that the logger does not
Expand Down
9 changes: 5 additions & 4 deletions inboard/start.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
from __future__ import annotations

import importlib.util
import json
import logging
import os
import subprocess
from pathlib import Path
from typing import Optional

import uvicorn # type: ignore

Expand Down Expand Up @@ -52,7 +53,7 @@ def set_gunicorn_options(app_module: str) -> list:
return ["gunicorn", "-k", worker_class, "-c", gunicorn_conf_path, app_module]


def _split_uvicorn_option(option: str) -> Optional[list]:
def _split_uvicorn_option(option: str) -> list | None:
return (
[option_item.strip() for option_item in str(option_value).split(sep=",")]
if (option_value := os.getenv(option.upper()))
Expand All @@ -77,7 +78,7 @@ def _update_uvicorn_config_options(uvicorn_config_options: dict) -> dict:
return uvicorn_config_options


def set_uvicorn_options(log_config: Optional[dict] = None) -> dict:
def set_uvicorn_options(log_config: dict | None = None) -> dict:
"""Set options for running the Uvicorn server."""
host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "80"))
Expand All @@ -99,7 +100,7 @@ def start_server(
process_manager: str,
app_module: str,
logger: logging.Logger = logging.getLogger(),
logging_conf_dict: Optional[dict] = None,
logging_conf_dict: dict | None = None,
) -> None:
"""Start the Uvicorn or Gunicorn server."""
try:
Expand Down
9 changes: 5 additions & 4 deletions tests/app/test_main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import sys
from typing import Dict, List

import pytest
from fastapi import FastAPI
Expand All @@ -14,7 +15,7 @@ class TestCors:
[Starlette CORS docs](https://www.starlette.io/middleware/#corsmiddleware).
"""

origins: Dict[str, List[str]] = {
origins: dict[str, list[str]] = {
"allowed": [
"http://br3ndon.land",
"https://br3ndon.land",
Expand All @@ -38,7 +39,7 @@ def test_cors_preflight_response_allowed(
self, allowed_origin: str, client: TestClient
) -> None:
"""Test pre-flight response to cross-origin request from allowed origin."""
headers: Dict[str, str] = {
headers: dict[str, str] = {
"Origin": allowed_origin,
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "X-Example",
Expand All @@ -54,7 +55,7 @@ def test_cors_preflight_response_disallowed(
self, disallowed_origin: str, client: TestClient
) -> None:
"""Test pre-flight response to cross-origin request from disallowed origin."""
headers: Dict[str, str] = {
headers: dict[str, str] = {
"Origin": disallowed_origin,
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "X-Example",
Expand Down
7 changes: 4 additions & 3 deletions tests/test_gunicorn_conf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import multiprocessing
import subprocess
from pathlib import Path
from typing import Optional

import pytest

Expand All @@ -20,7 +21,7 @@ def test_calculate_workers_default(self) -> None:
assert gunicorn_conf.workers == max(cores, 2)

@pytest.mark.parametrize("max_workers", (None, "1", "2", "5", "10"))
def test_calculate_workers_max(self, max_workers: Optional[str]) -> None:
def test_calculate_workers_max(self, max_workers: str | None) -> None:
"""Test Gunicorn worker process calculation with custom maximum."""
cores = multiprocessing.cpu_count()
default = max(cores, 2)
Expand All @@ -31,7 +32,7 @@ def test_calculate_workers_max(self, max_workers: Optional[str]) -> None:
assert result == default

@pytest.mark.parametrize("total_workers", (None, "1", "2", "5", "10"))
def test_calculate_workers_total(self, total_workers: Optional[str]) -> None:
def test_calculate_workers_total(self, total_workers: str | None) -> None:
"""Test Gunicorn worker process calculation with custom total."""
cores = multiprocessing.cpu_count()
result = gunicorn_conf.calculate_workers(None, total_workers)
Expand Down