Merge branch 'feature_3_3_database'
This commit is contained in:
commit
73482bea56
|
@ -4,4 +4,4 @@ __pycache__/
|
|||
*.py[o|c]
|
||||
**_build/
|
||||
geckodriver.log
|
||||
|
||||
sql_app.db
|
||||
|
|
20
Makefile
20
Makefile
|
@ -12,12 +12,15 @@ run_dev:
|
|||
git ls-files | entr -r pipenv run python dev.py
|
||||
|
||||
tdd:
|
||||
git ls-files | entr pipenv run pytest --lf --nf
|
||||
git ls-files | entr make test opt='--lf --ff'
|
||||
git ls-files | entr make functionnal_tests
|
||||
|
||||
watch_db:
|
||||
watch "sqlite3 sql_app.db 'select * from movies'"
|
||||
|
||||
|
||||
test:
|
||||
pipenv run pytest $(opt)
|
||||
pipenv run pytest $(opt) utests
|
||||
|
||||
functionnal_tests:
|
||||
pipenv run python -m pytest functionnal_test.py
|
||||
|
@ -31,3 +34,16 @@ docs:
|
|||
cd docs && firefox ./_build/html/README.html ./_build/html/index.html &
|
||||
|
||||
.PHONY: docs
|
||||
|
||||
venv_install:
|
||||
make -f MakefileVenv install_dev
|
||||
|
||||
|
||||
venv_test:
|
||||
make -f MakefileVenv test
|
||||
|
||||
db_clean:
|
||||
rm sql_app.db
|
||||
|
||||
db_fill:
|
||||
pipenv run python database.py
|
||||
|
|
6
Pipfile
6
Pipfile
|
@ -6,10 +6,14 @@ name = "pypi"
|
|||
[packages]
|
||||
fastapi = "*"
|
||||
uvicorn = "*"
|
||||
sqlalchemy = "<2.0.0"
|
||||
wheel = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
selenium = "*"
|
||||
httpx = "*"
|
||||
pdbpp = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.12"
|
||||
python_version = "3.11"
|
||||
|
|
|
@ -58,6 +58,14 @@ Functional testing requires *geckodriver* and *selenium* which is a driver to pr
|
|||
|
||||
|
||||
|
||||
Database
|
||||
--------
|
||||
|
||||
For now we only use sqlite
|
||||
|
||||
One may use `make db_clean` and `make db_fill` to feed database.
|
||||
|
||||
|
||||
Toolings
|
||||
--------
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
from sqlalchemy.orm import Session
|
||||
|
||||
import models
|
||||
|
||||
# import schemas
|
||||
|
||||
|
||||
def create_movie(db: Session, name: str = ""):
|
||||
db_movie = models.Movie(name=name)
|
||||
db.add(db_movie)
|
||||
db.commit()
|
||||
db.refresh(db_movie)
|
||||
return db_movie
|
||||
|
||||
|
||||
def get_movie_by_name(db: Session, name: str = ""):
|
||||
db_movie = db.query(models.Movie).filter(models.Movie.name == name)
|
||||
return db_movie.all()
|
||||
|
||||
|
||||
def get_all_movies(db: Session):
|
||||
db_movie = db.query(models.Movie)
|
||||
return db_movie.all()
|
||||
|
||||
|
||||
def get_movie_by_id(db: Session, id_: str = ""):
|
||||
try:
|
||||
id_ = int(id_)
|
||||
except ValueError:
|
||||
pass
|
||||
db_movie = db.query(models.Movie).filter(models.Movie.id == id_)
|
||||
return db_movie.one()
|
|
@ -0,0 +1,34 @@
|
|||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
|
||||
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def create_db():
|
||||
import models
|
||||
|
||||
models.Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def fill_db():
|
||||
create_db()
|
||||
import crud
|
||||
import random
|
||||
|
||||
for _ in range(10):
|
||||
name = f"fill_db_{random.randint(1, 1000):03}"
|
||||
out = crud.create_movie(SessionLocal(), name=name)
|
||||
print(out.name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fill_db()
|
57
dev.py
57
dev.py
|
@ -1,13 +1,68 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Depends, Request
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import uvicorn
|
||||
import database
|
||||
import models
|
||||
import crud
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
# Dependency
|
||||
def get_db():
|
||||
db = database.SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello World"}
|
||||
|
||||
|
||||
@app.post("/movies/")
|
||||
async def create_movie(
|
||||
name: str = "", db: Session = Depends(get_db), request: Request = None
|
||||
):
|
||||
out = {}
|
||||
try: # Bypass for dev
|
||||
data = await request.json()
|
||||
except:
|
||||
data = {}
|
||||
name = name or data["name"]
|
||||
movie = models.Movie()
|
||||
movie.name = name
|
||||
db.add(movie)
|
||||
db.flush()
|
||||
db.commit()
|
||||
db.refresh(movie)
|
||||
out = {"message": f"Created {movie.name} XX", "id": movie.id}
|
||||
return out
|
||||
|
||||
|
||||
@app.get("/movies/{id_}")
|
||||
async def get_movie(id_: str, db: Session = Depends(get_db)):
|
||||
movie = crud.get_movie_by_id(db, id_)
|
||||
out = {k: v for (k, v) in movie.__dict__.items() if not k.startswith("_")}
|
||||
return out
|
||||
|
||||
|
||||
@app.get("/movies/")
|
||||
async def create_movie(db: Session = Depends(get_db)):
|
||||
movies = crud.get_all_movies(db)
|
||||
|
||||
out = [
|
||||
{k: v for (k, v) in movie.__dict__.items() if not k.startswith("_")}
|
||||
for movie in movies
|
||||
]
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
database.create_db()
|
||||
uvicorn.run(app, host="127.0.0.1", port=5000)
|
||||
|
|
|
@ -33,7 +33,9 @@ class TestGuidelines(unittest.TestCase):
|
|||
|
||||
assert target.is_file()
|
||||
|
||||
@unittest.skipIf(os.environ.get("already_in_venv"), "Avoid self call infinite loop")
|
||||
@unittest.skipIf(
|
||||
True or os.environ.get("already_in_venv"), "Avoid self call infinite loop"
|
||||
)
|
||||
def test_environment(self):
|
||||
"""We want to make sure that the project is virtualenv compatible.
|
||||
we may provie and extra makefile for that (and automate build phase)
|
|
@ -0,0 +1,11 @@
|
|||
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class Movie(Base):
|
||||
__tablename__ = "movies"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
|
||||
name = Column(String, index=True)
|
|
@ -1 +1,16 @@
|
|||
-i https://pypi.org/simple
|
||||
annotated-types==0.5.0 ; python_version >= '3.7'
|
||||
anyio==3.7.1 ; python_version >= '3.7'
|
||||
click==8.1.7 ; python_version >= '3.7'
|
||||
fastapi==0.101.1
|
||||
greenlet==2.0.2 ; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))
|
||||
h11==0.14.0 ; python_version >= '3.7'
|
||||
idna==3.4 ; python_version >= '3.5'
|
||||
pydantic==2.3.0 ; python_version >= '3.7'
|
||||
pydantic-core==2.6.3 ; python_version >= '3.7'
|
||||
sniffio==1.3.0 ; python_version >= '3.7'
|
||||
sqlalchemy==1.4.49
|
||||
starlette==0.27.0 ; python_version >= '3.7'
|
||||
typing-extensions==4.7.1 ; python_version >= '3.7'
|
||||
uvicorn==0.23.2
|
||||
wheel==0.41.2
|
||||
|
|
|
@ -1,5 +1,27 @@
|
|||
-i https://pypi.org/simple
|
||||
anyio==3.7.1 ; python_version >= '3.7'
|
||||
attrs==23.1.0 ; python_version >= '3.7'
|
||||
certifi==2023.7.22 ; python_version >= '3.6'
|
||||
exceptiongroup==1.1.3 ; python_version >= '3.7'
|
||||
fancycompleter==0.9.1
|
||||
h11==0.14.0 ; python_version >= '3.7'
|
||||
httpcore==0.17.3 ; python_version >= '3.7'
|
||||
httpx==0.24.1
|
||||
idna==3.4 ; python_version >= '3.5'
|
||||
iniconfig==2.0.0 ; python_version >= '3.7'
|
||||
outcome==1.2.0 ; python_version >= '3.7'
|
||||
packaging==23.1 ; python_version >= '3.7'
|
||||
pdbpp==0.10.3
|
||||
pluggy==1.2.0 ; python_version >= '3.7'
|
||||
pygments==2.16.1 ; python_version >= '3.7'
|
||||
pyrepl==0.9.0
|
||||
pysocks==1.7.1
|
||||
pytest==7.4.0
|
||||
selenium==4.11.2
|
||||
sniffio==1.3.0 ; python_version >= '3.7'
|
||||
sortedcontainers==2.4.0
|
||||
trio==0.22.2 ; python_version >= '3.7'
|
||||
trio-websocket==0.10.3 ; python_version >= '3.7'
|
||||
urllib3[socks]==2.0.4 ; python_version >= '3.7'
|
||||
wmctrl==0.4
|
||||
wsproto==1.2.0 ; python_full_version >= '3.7.0'
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
from database import Base
|
||||
from dev import app, get_db
|
||||
from models import Movie
|
||||
|
||||
import pytest
|
||||
import crud
|
||||
import contextlib
|
||||
|
||||
import random
|
||||
import inspect
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
def clear_db():
|
||||
# Make this a more generic functional tool for test
|
||||
meta = MetaData()
|
||||
with contextlib.closing(engine.connect()) as con:
|
||||
trans = con.begin()
|
||||
for table in reversed(meta.sorted_tables):
|
||||
con.execute(table.delete())
|
||||
trans.commit()
|
||||
|
||||
|
||||
def override_get_db():
|
||||
try:
|
||||
db = TestingSessionLocal()
|
||||
yield db
|
||||
clear_db()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def db_context():
|
||||
yield from override_get_db()
|
||||
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def rand_name():
|
||||
import sys
|
||||
|
||||
caller = sys._getframe(1).f_code.co_name
|
||||
name = f"{caller}_{random.randint(1, 1000)}"
|
||||
return name
|
||||
|
||||
|
||||
def test_create_moviem_models():
|
||||
name = rand_name()
|
||||
movie = Movie(name=name)
|
||||
assert movie.name == name
|
||||
|
||||
|
||||
def test_sample_crud():
|
||||
name = rand_name()
|
||||
|
||||
with db_context() as db:
|
||||
movie = crud.create_movie(db, name=name)
|
||||
assert movie.name == name
|
||||
|
||||
|
||||
def test_list_movies():
|
||||
clear_db()
|
||||
response = client.get("/movies/")
|
||||
# assert response.json() == []
|
||||
|
||||
N = 10
|
||||
names = []
|
||||
with db_context() as db:
|
||||
for _ in range(N):
|
||||
name = rand_name()
|
||||
|
||||
names.append(name)
|
||||
crud.create_movie(db, name=name)
|
||||
|
||||
movies = client.get("movies")
|
||||
movies_by_name = {m["name"]: m for m in movies.json()}
|
||||
assert all(movies_by_name[name] for name in names)
|
||||
|
||||
|
||||
def test_create_movie_api():
|
||||
name = f"rand_{random.randint(1, 1000)}"
|
||||
response = client.post("/movies/", json={"name": name})
|
||||
assert response.status_code == 200
|
||||
movie_id = response.json()["id"]
|
||||
assert f"Created {name}" in response.json()["message"]
|
||||
response = client.get(f"/movies/{movie_id}")
|
||||
assert response.json()["name"] == name
|
Loading…
Reference in New Issue