Skip to content

Commit

Permalink
Python 3.10: Implement type-checking via pydantic-settings (#304)
Browse files Browse the repository at this point in the history
Python v3.10 support; FastAPI v0.104.1 bump.
  • Loading branch information
toddbirchard authored Nov 15, 2023
1 parent f53346f commit 6478a39
Show file tree
Hide file tree
Showing 41 changed files with 2,654 additions and 2,086 deletions.
Binary file removed .github/jamstack@2x.png
Binary file not shown.
137 changes: 66 additions & 71 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -1,79 +1,74 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python application

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

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install flake8 pytest-flask mock
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v0
with:
credentials_json: ${{ secrets.GOOGLE_CLOUD_JSON_KEY }}
- 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. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ENVIRONMENT: ${{ secrets.ENVIRONMENT }}
SQLALCHEMY_DATABASE_URI: ${{ secrets.SQLALCHEMY_DATABASE_URI }}
SQLALCHEMY_DATABASE_PEM: ${{ secrets.SQLALCHEMY_DATABASE_PEM }}
SQLALCHEMY_GHOST_DATABASE_NAME: ${{ secrets.SQLALCHEMY_GHOST_DATABASE_NAME }}
SQLALCHEMY_FEATURES_DATABASE_NAME: ${{ secrets.SQLALCHEMY_FEATURES_DATABASE_NAME }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
GOOGLE_CLOUD_JSON_KEY: ${{ secrets.GOOGLE_CLOUD_JSON_KEY }}
GOOGLE_CLOUD_PROJECT_NAME: ${{ secrets.GOOGLE_CLOUD_PROJECT_NAME }}
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
GCP_BIGQUERY_TABLE: ${{ secrets.GCP_BIGQUERY_TABLE }}
GCP_BIGQUERY_DATASET: ${{ secrets.GCP_BIGQUERY_DATASET }}
GCP_BUCKET_URL: ${{ secrets.GCP_BUCKET_URL }}
GCP_BUCKET_NAME: ${{ secrets.GCP_BUCKET_NAME }}
GHOST_BASE_URL: ${{ secrets.GHOST_BASE_URL }}
GHOST_ADMIN_BASE_URL: ${{ secrets.GHOST_ADMIN_BASE_URL }}
GHOST_CLIENT_ID: ${{ secrets.GHOST_CLIENT_ID }}
GHOST_ADMIN_API_KEY: ${{ secrets.GHOST_ADMIN_API_KEY }}
GHOST_CONTENT_API_KEY: ${{ secrets.GHOST_CONTENT_API_KEY }}
GHOST_API_USERNAME: ${{ secrets.GHOST_API_USERNAME }}
GHOST_API_PASSWORD: ${{ secrets.GHOST_API_PASSWORD }}
GHOST_NETLIFY_BUILD_HOOK: ${{ secrets.GHOST_NETLIFY_BUILD_HOOK }}
MAILGUN_EMAIL_SERVER: ${{ secrets.MAILGUN_EMAIL_SERVER }}
MAILGUN_NEWSLETTER_TEMPLATE: ${{ secrets.MAILGUN_NEWSLETTER_TEMPLATE }}
MAILGUN_SENDER_API_KEY: ${{ secrets.MAILGUN_SENDER_API_KEY }}
MAILGUN_FROM_SENDER: ${{ secrets.MAILGUN_FROM_SENDER }}
MAILGUN_PERSONAL_EMAIL: ${{ secrets.MAILGUN_PERSONAL_EMAIL }}
MAILGUN_PASSWORD: ${{ secrets.MAILGUN_PASSWORD }}
MIXPANEL_API_TOKEN: ${{ secrets.MIXPANEL_API_TOKEN }}
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
TWILIO_SENDER_PHONE: ${{ secrets.TWILIO_SENDER_PHONE }}
TWILIO_RECIPIENT_PHONE: ${{ secrets.TWILIO_RECIPIENT_PHONE }}
GH_USERNAME: ${{ secrets.GH_USERNAME }}
GH_API_KEY: ${{ secrets.GH_API_KEY }}
run: |
pytest
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"

- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
- name: "auth"
uses: "google-github-actions/auth@v1"
with:
credentials_json: ${{ secrets.GOOGLE_CREDENTIALS }}

- name: Lint with flake8
run: |
python3 -m pip install flake8
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
ENVIRONMENT: ${{ secrets.ENVIRONMENT }}
SQLALCHEMY_DATABASE_URI: ${{ secrets.SQLALCHEMY_DATABASE_URI }}
SQLALCHEMY_DATABASE_PEM: ${{ secrets.SQLALCHEMY_DATABASE_PEM }}
SQLALCHEMY_GHOST_DATABASE_NAME: ${{ secrets.SQLALCHEMY_GHOST_DATABASE_NAME }}
SQLALCHEMY_FEATURES_DATABASE_NAME: ${{ secrets.SQLALCHEMY_FEATURES_DATABASE_NAME }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
GOOGLE_CLOUD_JSON_KEY: ${{ secrets.GOOGLE_CLOUD_JSON_KEY }}
GOOGLE_CLOUD_PROJECT_NAME: ${{ secrets.GOOGLE_CLOUD_PROJECT_NAME }}
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
GCP_BIGQUERY_TABLE: ${{ secrets.GCP_BIGQUERY_TABLE }}
GCP_BIGQUERY_DATASET: ${{ secrets.GCP_BIGQUERY_DATASET }}
GCP_BUCKET_URL: ${{ secrets.GCP_BUCKET_URL }}
GCP_BUCKET_NAME: ${{ secrets.GCP_BUCKET_NAME }}
GHOST_BASE_URL: ${{ secrets.GHOST_BASE_URL }}
GHOST_ADMIN_BASE_URL: ${{ secrets.GHOST_ADMIN_BASE_URL }}
GHOST_CLIENT_ID: ${{ secrets.GHOST_CLIENT_ID }}
GHOST_ADMIN_API_KEY: ${{ secrets.GHOST_ADMIN_API_KEY }}
GHOST_CONTENT_API_KEY: ${{ secrets.GHOST_CONTENT_API_KEY }}
GHOST_API_USERNAME: ${{ secrets.GHOST_API_USERNAME }}
GHOST_API_PASSWORD: ${{ secrets.GHOST_API_PASSWORD }}
GHOST_NETLIFY_BUILD_HOOK: ${{ secrets.GHOST_NETLIFY_BUILD_HOOK }}
MAILGUN_EMAIL_SERVER: ${{ secrets.MAILGUN_EMAIL_SERVER }}
MAILGUN_NEWSLETTER_TEMPLATE: ${{ secrets.MAILGUN_NEWSLETTER_TEMPLATE }}
MAILGUN_SENDER_API_KEY: ${{ secrets.MAILGUN_SENDER_API_KEY }}
MAILGUN_FROM_SENDER: ${{ secrets.MAILGUN_FROM_SENDER }}
MAILGUN_PERSONAL_EMAIL: ${{ secrets.MAILGUN_PERSONAL_EMAIL }}
MAILGUN_PASSWORD: ${{ secrets.MAILGUN_PASSWORD }}
MIXPANEL_API_TOKEN: ${{ secrets.MIXPANEL_API_TOKEN }}
TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
TWILIO_SENDER_PHONE: ${{ secrets.TWILIO_SENDER_PHONE }}
TWILIO_RECIPIENT_PHONE: ${{ secrets.TWILIO_RECIPIENT_PHONE }}
GH_USERNAME: ${{ secrets.GH_USERNAME }}
GH_API_KEY: ${{ secrets.GH_API_KEY }}
run: |
python3 -m pip install pytest coverage
pytest
49 changes: 22 additions & 27 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,68 +19,64 @@ export HELP

.PHONY: run install deploy update format lint clean help


all help:
@echo "$$HELP"


env: $(VIRTUAL_ENV)


$(VIRTUAL_ENV):
if [ ! -d $(VIRTUAL_ENV) ]; then \
echo "Creating Python virtual env in \`${VIRTUAL_ENV}\`"; \
python3 -m venv $(VIRTUAL_ENV); \
fi


.PHONY: dev
dev: env
$(LOCAL_PYTHON) -m uvicorn app:api --reload

poetry config virtualenvs.path $(VIRTUAL_ENV)

.PHONY: run
run: env
$(LOCAL_PYTHON) -m asgi
$(LOCAL_PYTHON) -m uvicorn asgi:api --port 9300 --workers 4

.PHONY: dev
dev: env
$(LOCAL_PYTHON) -m uvicorn asgi:api --reload --port 9300

.PHONY: install
install: env
$(shell . $(VIRTUAL_ENV)/bin/activate)
$(LOCAL_PYTHON) -m pip install --upgrade pip setuptools wheel && \
LDFLAGS="-L$(/opt/homebrew/bin/brew --prefix openssl)/lib -L$(/opt/homebrew/bin/brew --prefix re2)/lib" CPPFLAGS="-I$(/opt/homebrew/bin/brew --prefix openssl)/include -I$(/opt/homebrew/bin/brew --prefix re2)/include" GRPC_BUILD_WITH_BORING_SSL_ASM="" GRPC_PYTHON_BUILD_SYSTEM_RE2=true GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true GRPC_PYTHON_BUILD_SYSTEM_ZLIB=true pip install grpcio && \
$(LOCAL_PYTHON) -m pip install -r requirements.txt && \
LDFLAGS="-L$(/opt/homebrew/bin/brew --prefix openssl)/lib -L$(/opt/homebrew/bin/brew --prefix re2)/lib" && \
CPPFLAGS="-I$(/opt/homebrew/bin/brew --prefix openssl)/include -I$(/opt/homebrew/bin/brew --prefix re2)/include" && \
GRPC_BUILD_WITH_BORING_SSL_ASM="" && \
GRPC_PYTHON_BUILD_SYSTEM_RE2=true && \
GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true && \
GRPC_PYTHON_BUILD_SYSTEM_ZLIB=true && \
poetry install --with dev --sync
echo Installed dependencies in \`${VIRTUAL_ENV}\`;


.PHONY: deploy
deploy:
make install \
make install && \
make run


.PHONY: test
test: env
$(LOCAL_PYTHON) -m \
coverage run -m pytest -vv \
--disable-pytest-warnings && \
coverage html --title='Coverage Report' -d .reports && \
poetry config virtualenvs.path $(VIRTUAL_ENV)
$(LOCAL_PYTHON) -m coverage run -m pytest -vv \
--disable-pytest-warnings
$(LOCAL_PYTHON) -m coverage html --title='Coverage Report' -d .reports && \
open .reports/index.html


.PHONY: update
update: env
$(LOCAL_PYTHON) -m pip install --upgrade pip setuptools wheel && \
poetry update && \
poetry update --with dev && \
poetry export -f requirements.txt --output requirements.txt --without-hashes && \
echo Installed dependencies in \`${VIRTUAL_ENV}\`;


.PHONY: format
format: env
$(LOCAL_PYTHON) -m isort --multi-line=3 .
$(LOCAL_PYTHON) -m isort --multi-line=3 . && \
$(LOCAL_PYTHON) -m black .


.PHONY: lint
lint: env
$(LOCAL_PYTHON) -m flake8 . --count \
Expand All @@ -89,15 +85,14 @@ lint: env
--show-source \
--statistics


.PHONY: clean
clean:
find . -name 'poetry.lock' -delete && \
find . -name '.coverage' -delete && \
find . -wholename '**/*.pyc' -delete && \
find . -type d -wholename '__pycache__' -exec rm -rf {} + && \
find . -type d -wholename '.venv' -exec rm -rf {} + && \
find . -type d -wholename './.venv' -exec rm -rf {} + && \
find . -type d -wholename '.pytest_cache' -exec rm -rf {} + && \
find . -type d -wholename '**/.pytest_cache' -exec rm -rf {} + && \
find . -type d -wholename './logs/*' -exec rm -rf {} + && \
find . -type d -wholename './logs/*.log' -exec rm -rf {} + && \
find . -type d -wholename './.reports/*' -exec rm -rf {} +
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Blog Webhook API

![Python](https://img.shields.io/badge/Python-^3.9-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a)
![FastAPI](https://img.shields.io/badge/FastAPI-^v0.87.0-blue.svg?longCache=true&logo=fastapi&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
![FastAPI](https://img.shields.io/badge/FastAPI-^v0.89.1-blue.svg?longCache=true&logo=fastapi&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
![Databases](https://img.shields.io/badge/Databases-^0.7.0-blue.svg?logo=python&longCache=true&logoColor=white&colorB=5e81ac&style=flat-square&colorA=4c566a)
![PyDantic](https://img.shields.io/badge/Pydantic-^v1.10.4-blue.svg?longCache=true&logo=python&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
![PyDantic](https://img.shields.io/badge/Pydantic-^v1.10.6-blue.svg?longCache=true&logo=python&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a)
![Google Cloud](https://img.shields.io/badge/Google--Cloud-^v0.34.0-lightgrey.svg?longCache=true&style=flat-square&logo=googlecloud&logoColor=white&colorB=5e81ac&colorA=4c566a)
![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-^1.4.45-red.svg?longCache=true&style=flat-square&logo=scala&logoColor=white&colorA=4c566a&colorB=bf616a)
![SQLAlchemy](https://img.shields.io/badge/SQLAlchemy-^2.0.21-red.svg?longCache=true&style=flat-square&logo=scala&logoColor=white&colorA=4c566a&colorB=bf616a)
![Ghost](https://img.shields.io/badge/Ghost-^v5.0.0-lightgrey.svg?longCache=true&style=flat-square&logo=ghost&logoColor=white&colorB=656c82&colorA=4c566a)
![GitHub Last Commit](https://img.shields.io/github/last-commit/google/skia.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=a3be8c)
[![GitHub Issues](https://img.shields.io/github/issues/toddbirchard/blog-webhook-api.svg?style=flat-square&colorA=4c566a&logo=GitHub&colorB=ebcb8b)](https://github.com/toddbirchard/blog-webhook-api/issues)
Expand Down
81 changes: 50 additions & 31 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,61 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app import accounts, analytics, authors, donations, github, images, newsletter, posts, tags
from app import (
accounts,
analytics,
authors,
donations,
github,
images,
newsletter,
posts,
tags,
)
from config import settings
from database.orm import Base, engine
from log import LOGGER

Base.metadata.create_all(bind=engine)

# Initialize API
api = FastAPI(
title="Blog Webhook API",
description="Webhook-driven API to make maintaining blogs easier.",
version="0.1.0",
debug=True,
docs_url="/",
openapi_url="/api.json",
openapi_tags=settings.API_TAGS,
)

# Define Middleware
api.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def create_app() -> FastAPI:
"""
Initialize API application.
:returns: FastAPI
"""
api = FastAPI(
title="Blog Webhook API",
description="Webhook-driven API to make maintaining blogs easier.",
version="0.1.0",
debug=True,
docs_url="/",
openapi_url="/api.json",
)

# Define Middleware
api.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Include routers
api.include_router(analytics.router)
api.include_router(newsletter.router)
api.include_router(posts.router)
api.include_router(accounts.router)
api.include_router(authors.router)
api.include_router(donations.router)
api.include_router(images.router)
api.include_router(tags.router)
api.include_router(github.router)
LOGGER.success("API successfully started.")

return api


# Include routers
api.include_router(analytics.router)
api.include_router(newsletter.router)
api.include_router(posts.router)
api.include_router(accounts.router)
api.include_router(authors.router)
api.include_router(donations.router)
api.include_router(images.router)
api.include_router(tags.router)
api.include_router(github.router)

LOGGER.success("API successfully started.")
api = create_app()
Loading

0 comments on commit 6478a39

Please sign in to comment.