Skip to content

Commit

Permalink
Tests, schemas, forms, etc
Browse files Browse the repository at this point in the history
  • Loading branch information
smbrine committed Jan 16, 2024
1 parent 9a43faa commit 0c214e4
Show file tree
Hide file tree
Showing 15 changed files with 164 additions and 103 deletions.
9 changes: 0 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
repos:
- repo: local
hooks:
- id: pip-freeze
name: Pip Freeze
entry: bash -c 'source .venv/bin/activate && pip freeze > requirements.txt'
language: system
always_run: true
stages: [ commit ]

- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ postgres:

run:
echo "THIS COMMAND ONLY WORKS ON MAC AND IF YOU USE VENV WITH NAME .env"
source .venv/bin/activate && export "PYTHONPATH=./" && python app/main.py
source .venv/bin/activate && export "PYTHONPATH=./" && python app/main.py

test:
export "PYTHONPATH=./" && pytest tests
16 changes: 9 additions & 7 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ssl
from contextlib import asynccontextmanager

import uvicorn
Expand Down Expand Up @@ -36,16 +35,19 @@ async def lifespan(fastapi_app: FastAPI):

app.include_router(v1)


@app.get("/")
async def root():
return {"message": "alive!"}


if __name__ == "__main__":
print(settings.SSL_CERT_PATH, settings.SSL_KEY_PATH)
if settings.IS_HTTPS:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(
settings.SSL_CERT_PATH, keyfile=settings.SSL_KEY_PATH
)
uvicorn.run(
"app.main:app",
host=settings.SERVER_BIND_HOSTNAME,
port=settings.SERVER_BIND_PORT,
port=int(settings.SERVER_BIND_PORT),
reload=True,
ssl_certfile=settings.SSL_CERT_PATH,
ssl_keyfile=settings.SSL_KEY_PATH,
Expand All @@ -54,6 +56,6 @@ async def lifespan(fastapi_app: FastAPI):
uvicorn.run(
"app.main:app",
host=settings.SERVER_BIND_HOSTNAME,
port=settings.SERVER_BIND_PORT,
port=int(settings.SERVER_BIND_PORT),
reload=True,
)
21 changes: 15 additions & 6 deletions app/routers/photo_router.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import io
from json import JSONDecodeError

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Body, Depends, File, Form, HTTPException, UploadFile
from pydantic import Json
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import Response, StreamingResponse
from fastapi.responses import Response, StreamingResponse
from typing import Annotated, Optional
import json

from app import schemas
from db import models
Expand All @@ -20,17 +24,22 @@ def render(self, content: bytes) -> bytes:

@router.post("/add")
async def add_photo(
data: schemas.PhotoAdd = Depends(),
data: schemas.PhotoAdd = Depends(schemas.PhotoAddForm),
photo: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
):
extension = data.object.filename.split(".")[-1]
photo_data = await data.object.read()
photo_data = photo.file.read()
extension = photo.filename.split(".")[-1]

# Create the photo instance and add it to the database
result = await models.Photo.create(
db,
object=photo_data,
extension=extension,
owner_id="uuid-for-user",
**data.model_dump(exclude=["object", "identifier"]),
)

return schemas.PhotoReturn(**result.__dict__)


Expand All @@ -42,7 +51,7 @@ async def get_photos(db: AsyncSession = Depends(get_db), skip: int = 0, limit: i

@router.get("/{filename}")
async def get_photo(filename: str, db: AsyncSession = Depends(get_db)):
identifier, extension = filename.split(".")
identifier, _ = filename.split(".")
photo = await models.Photo.get(db, identifier)
result = photo.object

Expand Down
2 changes: 1 addition & 1 deletion app/routers/project_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

@router.post("/start")
async def start_project(
data: schemas.ProjectStart = Depends(),
data: schemas.ProjectStart,
db: AsyncSession = Depends(get_db),
):
result = await models.Project.create(db, **data.model_dump())
Expand Down
2 changes: 1 addition & 1 deletion app/routers/record_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ async def get_all_records(


@router.get("/s/geojson")
async def get_all_records(
async def get_all_records_geojson(
db: AsyncSession = Depends(get_db), skip: int = 0, limit: int = 20
):
records = await models.Record.get_all(db, skip=skip, limit=limit)
Expand Down
39 changes: 29 additions & 10 deletions app/schemas/photo_schemas.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
from datetime import date, datetime
from typing import Optional
from typing import Annotated, Optional

from fastapi import File, Form, UploadFile
from pydantic import BaseModel
from app.schemas.utils import as_form


class Photo(BaseModel):
Expand All @@ -13,14 +15,31 @@ class Photo(BaseModel):


class PhotoAdd(Photo):
object: UploadFile = File(...)
record_id: str = Form(...)
description: str = Form("No description")
latitude: Optional[float] = Form(None)
longitude: Optional[float] = Form(None)
owner_id: str = Form(...)
record_id: str = "uuid-for-record"
description: str = "No description"
latitude: float = 0.0
longitude: float = 0.0

date_taken: date = date.today()


@as_form
class PhotoAddForm(Photo):
record_id: str = "uuid-for-record"
description: str = "No description"
latitude: float = 0.0
longitude: float = 0.0

date_taken: date = Form(...)
date_taken: date = date.today()
# @classmethod
# def __get_validators__(cls):
# yield cls.validate_to_json
#
# @classmethod
# def validate_to_json(cls, value):
# if isinstance(value, str):
# return cls(**json.loads(value))
# return value


class PhotoPublic(Photo):
Expand All @@ -37,7 +56,7 @@ class PhotoReturn(Photo):

extension: Optional[str] = None

class Config:
class ConfigDict:
from_attributes = True


Expand All @@ -49,5 +68,5 @@ class PhotoInDB(Photo):
extension: str
owner_id: str

class Config:
class ConfigDict:
from_attributes = True
12 changes: 9 additions & 3 deletions app/schemas/project_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic import BaseModel

from app.schemas import RecordInDB, RecordReturn, RecordCreate
from app.schemas.utils import as_form


class Project(BaseModel):
Expand All @@ -17,9 +18,14 @@ class Project(BaseModel):


class ProjectStart(Project):
records: List[RecordCreate] | None = Form(...)
name: str = Form(...)
owner_id: str = Form(...)
name: str
owner_id: str


@as_form
class ProjectStartForm(Project):
name: str
owner_id: str


class ProjectInDB(Project):
Expand Down
14 changes: 14 additions & 0 deletions app/schemas/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from fastapi import Form


def as_form(cls):
"""decorator for you pydantic model to turn them into form models"""
cls.__signature__ = cls.__signature__.replace(
parameters=[
arg.replace(
default=Form(...) if arg.default is arg.empty else Form(arg.default)
)
for arg in cls.__signature__.parameters.values()
]
)
return cls
18 changes: 12 additions & 6 deletions app/settings.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import os
from typing import Any

import pathlib
from dotenv import load_dotenv
from pydantic import field_validator
from pydantic_settings import BaseSettings

import ssl

load_dotenv()


class Settings(BaseSettings):
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./db.sqlite3")
SERVER_BIND_HOSTNAME: str = os.getenv("SERVER_BIND_HOSTNAME", "127.0.0.1")
SERVER_BIND_PORT: int = os.getenv("SERVER_BIND_PORT", 8000)
SERVER_BIND_PORT: int = int(os.getenv("SERVER_BIND_PORT", 8000))

SSL_CERT_PATH: str = os.getenv("SSL_CERT_PATH", "./ssl/cert.pem")
SSL_KEY_PATH: str = os.getenv("SSL_KEY_PATH", "./ssl/key.pem")
IS_HTTPS: bool = os.getenv("IS_HTTPS", 0)
IS_HTTPS: bool = bool(int(os.getenv("IS_HTTPS", 0)))

@field_validator("SSL_CERT_PATH", "SSL_KEY_PATH")
@classmethod
def provide_with_abspath(cls, v: str) -> str:
if pathlib.Path(v).is_absolute():
return v
return str(pathlib.Path(__file__).parents[1]) + v.replace("./", "/")


settings = Settings()
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ annotated-types==0.6.0
anyio==4.2.0
astroid==3.0.2
asyncpg==0.29.0
attrs==23.2.0
black==23.12.1
certifi==2023.11.17
cffi==1.16.0
Expand All @@ -29,6 +30,7 @@ mccabe==0.7.0
mirakuru==2.5.2
mypy-extensions==1.0.0
nodeenv==1.8.0
outcome==1.3.0.post0
packaging==23.2
pathspec==0.12.1
platformdirs==4.1.0
Expand All @@ -49,9 +51,11 @@ python-multipart==0.0.6
PyYAML==6.0.1
setuptools==69.0.3
sniffio==1.3.0
sortedcontainers==2.4.0
SQLAlchemy==2.0.25
starlette==0.32.0.post1
tomlkit==0.12.3
trio==0.24.0
trustme==1.1.0
typing_extensions==4.9.0
uvicorn==0.25.0
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import pytest


@pytest.fixture
def anyio_backend():
return "asyncio"
17 changes: 0 additions & 17 deletions tests/run_tests.py

This file was deleted.

42 changes: 0 additions & 42 deletions tests/test_crud/test_crud_photo.py

This file was deleted.

Loading

0 comments on commit 0c214e4

Please sign in to comment.