Skip to content

Commit 949dd71

Browse files
authored
Merge pull request #3 from rafsaf/small_fixes_around
Move sqlalchemy2-stubs to dev dependencies, add few missing types, a bit better python code style, added tests workflow and froze supported python version to ^3.7
2 parents 995316e + 503aa5c commit 949dd71

File tree

15 files changed

+435
-1268
lines changed

15 files changed

+435
-1268
lines changed

.github/workflows/tests.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-20.04
10+
strategy:
11+
matrix:
12+
python-version: [3.7, 3.8, 3.9]
13+
14+
services:
15+
postgres:
16+
image: postgres
17+
env:
18+
POSTGRES_DB: test
19+
POSTGRES_USER: test
20+
POSTGRES_PASSWORD: test
21+
options: >-
22+
--health-cmd pg_isready
23+
--health-interval 10s
24+
--health-timeout 5s
25+
--health-retries 5
26+
ports:
27+
- 30000:5432
28+
steps:
29+
- uses: actions/checkout@v2
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@v2
33+
with:
34+
python-version: ${{ matrix.python-version }}
35+
- name: Load cached venv
36+
id: cached-poetry-dependencies
37+
uses: actions/cache@v2
38+
with:
39+
path: .venv
40+
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
41+
- name: Install dependencies and actiavte virtualenv
42+
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
43+
run: |
44+
python -m venv .venv
45+
source .venv/bin/activate
46+
pip install -r {{cookiecutter.project_name}}/requirements-dev.txt
47+
pip install cookiecutter
48+
- name: Lint with flake8
49+
run: |
50+
source .venv/bin/activate
51+
# stop the build if there are Python syntax errors or undefined names
52+
cd \{\{cookiecutter.project_name\}\}/
53+
flake8 app --count --exit-zero --statistics
54+
- name: Test new cookiecuttered project is passing pytest test
55+
run: |
56+
source .venv/bin/activate
57+
python tests/create_test_project.py
58+
export TEST_DATABASE_HOSTNAME=localhost
59+
export TEST_DATABASE_USER=test
60+
export TEST_DATABASE_PASSWORD=test
61+
export TEST_DATABASE_PORT=30000
62+
export TEST_DATABASE_DB=test
63+
64+
pytest generated_project_for_tests

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
<a href="https://github.com/rafsaf/minimal-fastapi-postgres-template/actions?query=workflow%3Atests" target="_blank">
2+
<img src="https://github.com/rafsaf/minimal-fastapi-postgres-template/workflows/tests/badge.svg" alt="Test">
3+
</a>
4+
5+
<a href="https://github.com/rafsaf/respo/blob/main/LICENSE" target="_blank">
6+
<img src="https://img.shields.io/github/license/rafsaf/respo" alt="License">
7+
</a>
8+
19
## Minimal async FastAPI + postgresql template
210

311
![OpenAPIexample](./docs/OpenAPI_example.png)

tests/__init__.py

Whitespace-only changes.

tests/create_test_project.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Creates template project in root folder with default values
3+
"""
4+
5+
from pathlib import Path
6+
7+
from cookiecutter.main import cookiecutter
8+
9+
ROOT_FOLDER = Path(__file__).parent.parent
10+
11+
12+
def main():
13+
cookiecutter(
14+
template=str(ROOT_FOLDER),
15+
no_input=True,
16+
extra_context={
17+
"project_name": "generated_project_for_tests",
18+
},
19+
)
20+
21+
22+
if __name__ == "__main__":
23+
main()

{{cookiecutter.project_name}}/app/api/deps.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,3 @@ async def get_current_user(
4242
if not user:
4343
raise HTTPException(status_code=404, detail="User not found")
4444
return user
45-
46-
47-
async def get_current_active_user(
48-
current_user: User = Depends(get_current_user),
49-
) -> User:
50-
return current_user

{{cookiecutter.project_name}}/app/api/endpoints/users.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from typing import Any
2-
31
from fastapi import APIRouter, Depends
42
from sqlalchemy.ext.asyncio import AsyncSession
53

@@ -14,8 +12,8 @@
1412
async def update_user_me(
1513
user_update: schemas.UserUpdate,
1614
session: AsyncSession = Depends(deps.get_session),
17-
current_user: models.User = Depends(deps.get_current_active_user),
18-
) -> Any:
15+
current_user: models.User = Depends(deps.get_current_user),
16+
):
1917
"""
2018
Update current user.
2119
"""
@@ -35,8 +33,8 @@ async def update_user_me(
3533

3634
@router.get("/me", response_model=schemas.User)
3735
async def read_user_me(
38-
current_user: models.User = Depends(deps.get_current_active_user),
39-
) -> Any:
36+
current_user: models.User = Depends(deps.get_current_user),
37+
):
4038
"""
4139
Get current user.
4240
"""

{{cookiecutter.project_name}}/app/core/config.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,18 @@
2020
"""
2121

2222
from pathlib import Path
23-
from typing import Dict, List, Literal, Union
23+
from typing import Dict, List, Union
2424

2525
import toml
2626
from pydantic import AnyHttpUrl, AnyUrl, BaseSettings, EmailStr, validator
2727

28+
# Literal from typing_extensions for python 3.7 support, remove if not needed
29+
try:
30+
from typing import Literal
31+
except ImportError:
32+
from typing_extensions import Literal
33+
#
34+
2835
PROJECT_DIR = Path(__file__).parent.parent.parent
2936
pyproject_content = toml.load(f"{PROJECT_DIR}/pyproject.toml")["tool"]["poetry"]
3037

@@ -64,7 +71,7 @@ class Settings(BaseSettings):
6471

6572
# VALIDATORS
6673
@validator("BACKEND_CORS_ORIGINS")
67-
def _assemble_cors_origins(cls, cors_origins):
74+
def _assemble_cors_origins(cls, cors_origins: Union[str, List[AnyHttpUrl]]):
6875
if isinstance(cors_origins, str):
6976
return [item.strip() for item in cors_origins.split(",")]
7077
return cors_origins

{{cookiecutter.project_name}}/app/core/security.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
from datetime import datetime, timedelta
8-
from typing import Any, Union
8+
from typing import Any, Tuple, Union
99

1010
from jose import jwt
1111
from passlib.context import CryptContext
@@ -18,7 +18,7 @@
1818
ALGORITHM = "HS256"
1919

2020

21-
def create_access_token(subject: Union[str, Any]) -> tuple[str, datetime]:
21+
def create_access_token(subject: Union[str, Any]) -> Tuple[str, datetime]:
2222
now = datetime.utcnow()
2323
expire = now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
2424

@@ -31,7 +31,7 @@ def create_access_token(subject: Union[str, Any]) -> tuple[str, datetime]:
3131
return encoded_jwt, expire
3232

3333

34-
def create_refresh_token(subject: Union[str, Any]) -> tuple[str, datetime]:
34+
def create_refresh_token(subject: Union[str, Any]) -> Tuple[str, datetime]:
3535
now = datetime.utcnow()
3636
expire = now + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES)
3737

{{cookiecutter.project_name}}/app/session.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from app.core.config import settings
55

66
if settings.ENVIRONMENT == "PYTEST":
7-
SQLALCHEMY_DATABASE_URI = settings.TEST_SQLALCHEMY_DATABASE_URI
7+
sqlalchemy_database_uri = settings.TEST_SQLALCHEMY_DATABASE_URI
88
else:
9-
SQLALCHEMY_DATABASE_URI = settings.DEFAULT_SQLALCHEMY_DATABASE_URI
9+
sqlalchemy_database_uri = settings.DEFAULT_SQLALCHEMY_DATABASE_URI
1010

11-
async_engine = create_async_engine(SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
11+
async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True)
1212
async_session = sessionmaker(async_engine, expire_on_commit=False, class_=AsyncSession)

{{cookiecutter.project_name}}/app/tests/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
import string
33

44

5-
def random_lower_string(length=32) -> str:
5+
def random_lower_string(length: int = 32) -> str:
66
return "".join(random.choices(string.ascii_lowercase, k=length))
77

88

9-
def random_email(length=10) -> str:
9+
def random_email(length: int = 10) -> str:
1010
return f"{random_lower_string(length)}@{random_lower_string(length)}.com"

0 commit comments

Comments
 (0)