This commit is contained in:
Waylon Walker 2023-05-19 14:11:01 -05:00
parent 4be274d9e2
commit c238b9d757
No known key found for this signature in database
GPG key ID: 66E2BF2B4190EFE4
21 changed files with 219 additions and 184 deletions

1
.gitignore vendored
View file

@ -962,3 +962,4 @@ FodyWeavers.xsd
# Additional files built by Visual Studio # Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode # End of https://www.toptal.com/developers/gitignore/api/vim,node,data,emacs,python,pycharm,executable,sublimetext,visualstudio,visualstudiocode
database.db

Binary file not shown.

View file

@ -1,10 +1,11 @@
from typing import Union from typing import Union
from fastapi import FastAPI from fastapi import FastAPI
import httpx import httpx
from learn_sql_model.console import console from learn_sql_model.console import console
from learn_sql_model.models import Hero, Pet from learn_sql_model.models.hero import Hero
from learn_sql_model.models.pet import Pet
models = Union[Hero, Pet] models = Union[Hero, Pet]

View file

@ -3,7 +3,7 @@ from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from learn_sql_model.api.user import oauth2_scheme from learn_sql_model.api.user import oauth2_scheme
from learn_sql_model.models import Hero from learn_sql_model.models.hero import Hero
hero_router = APIRouter() hero_router = APIRouter()

View file

@ -2,6 +2,7 @@ import typer
import uvicorn import uvicorn
from learn_sql_model.cli.common import verbose_callback from learn_sql_model.cli.common import verbose_callback
from learn_sql_model.config import config
api_app = typer.Typer() api_app = typer.Typer()
@ -25,4 +26,4 @@ def run(
help="show the log messages", help="show the log messages",
), ),
): ):
uvicorn.run("learn_sql_model.api.app:app", port=5000, log_level="info") uvicorn.run("learn_sql_model.api.app:app", port=config.port, log_level="info")

View file

@ -4,7 +4,7 @@ from learn_sql_model.cli.api import api_app
from learn_sql_model.cli.common import verbose_callback from learn_sql_model.cli.common import verbose_callback
from learn_sql_model.cli.config import config_app from learn_sql_model.cli.config import config_app
from learn_sql_model.cli.hero import hero_app from learn_sql_model.cli.hero import hero_app
from learn_sql_model.cli.model_app import model_app from learn_sql_model.cli.model import model_app
from learn_sql_model.cli.tui import tui_app from learn_sql_model.cli.tui import tui_app
app = typer.Typer( app = typer.Typer(

View file

@ -4,7 +4,7 @@ from pydantic_typer import expand_pydantic_args
from rich.console import Console from rich.console import Console
import typer import typer
from learn_sql_model.models import Hero from learn_sql_model.models.hero import Hero
hero_app = typer.Typer() hero_app = typer.Typer()

View file

@ -0,0 +1,50 @@
import typer
from learn_sql_model.cli.common import verbose_callback
model_app = typer.Typer()
@model_app.callback()
def model(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
"model cli"
@model_app.command()
def create_revision(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
message: str = typer.Option(
prompt=True,
),
):
import alembic
# python -m alembic revision --autogenerate -m "New Attribute"
from alembic.config import Config
alembic_cfg = Config("alembic.ini")
alembic.command.revision(
config=alembic_cfg,
message=message,
autogenerate=True,
)
@model_app.command()
def populate(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
...

View file

@ -1,119 +0,0 @@
from rich.console import Console
from sqlmodel import SQLModel, Session
import typer
from learn_sql_model.cli.common import verbose_callback
from learn_sql_model.config import config
from learn_sql_model.models import Hero, Pet
model_app = typer.Typer()
@model_app.callback()
def model(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
"model cli"
@model_app.command()
def create_revision(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
message: str = typer.Option(
prompt=True,
),
):
import alembic
# python -m alembic revision --autogenerate -m "New Attribute"
from alembic.config import Config
alembic_cfg = Config("alembic.ini")
alembic.command.revision(
config=alembic_cfg,
message=message,
autogenerate=True,
)
@model_app.command()
def show(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
SQLModel.metadata.create_all(config.engine)
with Session(config.engine) as session:
heros = session.exec(select(Hero)).all()
Console().print(heros)
@model_app.command()
def read(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
from learn_sql_model.api import read_heroes
Console().print(read_heroes())
# @model_app.command()
# @expand_pydantic_args(typer=True)
# def create(
# hero: Hero,
# ):
# hero.post()
# try:
# httpx.post("http://localhost:5000/heroes/", json=hero.dict())
# except httpx.ConnectError:
# console.log("local failover")
# with Session(config.engine) as session:
# session.add(hero)
# session.commit()
@model_app.command()
def populate(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
pet_1 = Pet(name="Deadpond-Dog")
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson", pets=[pet_1])
hero_2 = Hero(
name="Spider-Boy",
secret_name="Pedro Parqueador",
pet=Pet(name="Spider-Boy-Dog"),
)
hero_3 = Hero(
name="Rusty-Man",
secret_name="Tommy Sharp",
age=48,
pet=Pet(name="Rusty-Man-Dog"),
)
SQLModel.metadata.create_all(config.engine)
with Session(config.engine) as session:
session.add(hero_1)
session.add(hero_2)
session.add(hero_3)
session.commit()

View file

@ -3,7 +3,8 @@ from typing import TYPE_CHECKING
from pydantic import BaseSettings from pydantic import BaseSettings
from sqlmodel import SQLModel, Session, create_engine from sqlmodel import SQLModel, Session, create_engine
from learn_sql_model.models import Hero, Pet from learn_sql_model.models.hero import Hero
from learn_sql_model.models.pet import Pet
from learn_sql_model.standard_config import load from learn_sql_model.standard_config import load
models = [Hero, Pet] models = [Hero, Pet]
@ -14,6 +15,7 @@ if TYPE_CHECKING:
class Config(BaseSettings): class Config(BaseSettings):
database_url: str = "sqlite:///database.db" database_url: str = "sqlite:///database.db"
port: int = 5000
class Config: class Config:
env_prefix = "LEARN_SQL_MODEL_" env_prefix = "LEARN_SQL_MODEL_"
@ -35,6 +37,11 @@ class Config(BaseSettings):
# app.get("/heroes/")(Hero.read_heroes) # app.get("/heroes/")(Hero.read_heroes)
def get_config(overrides: dict = {}) -> Config:
raw_config = load("learn_sql_model") raw_config = load("learn_sql_model")
config = Config(**raw_config) config = Config(**raw_config, **overrides)
config.create_db_and_tables() config.create_db_and_tables()
return config
config = get_config()

View file

View file

@ -0,0 +1,7 @@
from polyfactory.factories.pydantic_factory import ModelFactory
from learn_sql_model.models.hero import Hero
class HeroFactory(ModelFactory[Hero]):
__model__ = Hero

View file

@ -1,43 +0,0 @@
from __future__ import annotations
from typing import Optional
from sqlmodel import Field, SQLModel, select
class FastModel(SQLModel):
def post(self):
from learn_sql_model.config import config
with config.session as session:
session.add(self)
session.commit()
@classmethod
def get(self, item_id: int = None):
from learn_sql_model.config import config
with config.session as session:
if item_id is None:
return session.exec(select(self)).all()
return session.exec(select(self).where(self.id == item_id)).one()
class Hero(FastModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
secret_name: str
age: Optional[int] = None
# new_attribute: Optional[str] = None
# pets: List["Pet"] = Relationship(back_populates="hero")
class Pet(FastModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = "Jim"
# age: Optional[int] = None
# hero_id: int = Field(default=None, foreign_key="hero.id")
# hero: Optional[Hero] = Relationship(back_populates="pets")

View file

View file

@ -0,0 +1,46 @@
from typing import Optional, TYPE_CHECKING
from sqlmodel import SQLModel, select
if TYPE_CHECKING:
from learn_sql_model.config import Config
class FastModel(SQLModel):
def pre_post(self) -> None:
"""run before post"""
def pre_delete(self) -> None:
"""run before delete"""
@classmethod
def pre_get(self) -> None:
"""run before get"""
def post(self, config: "Config" = None) -> None:
if config is None:
from learn_sql_model.config import get_config
config = get_config()
self.pre_post()
with config.session as session:
session.add(self)
session.commit()
@classmethod
def get(
self, item_id: int = None, config: "Config" = None
) -> Optional["FastModel"]:
if config is None:
from learn_sql_model.config import get_config
config = get_config()
self.pre_get()
with config.session as session:
if item_id is None:
return session.exec(select(self)).all()
return session.exec(select(self).where(self.id == item_id)).one()

View file

@ -0,0 +1,13 @@
from typing import Optional
from sqlmodel import Field
from learn_sql_model.models.fast_model import FastModel
class Hero(FastModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
secret_name: str
age: Optional[int] = None
shoe_size: Optional[int] = None

View file

@ -0,0 +1,16 @@
from typing import Optional
from sqlmodel import Field
from learn_sql_model.models.fast_model import FastModel
class Pet(FastModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = "Jim"
# age: Optional[int] = None
# hero_id: int = Field(default=None, foreign_key="hero.id")
# hero: Optional[Hero] = Relationship(back_populates="pets")

View file

@ -2,10 +2,11 @@ from logging.config import fileConfig
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from learn_sql_model.models import Hero, Pet
from sqlmodel import SQLModel from sqlmodel import SQLModel
from learn_sql_model.models.hero import Hero
from learn_sql_model.models.pet import Pet
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.
config = context.config config = context.config

View file

@ -0,0 +1,29 @@
"""add shoe size
Revision ID: 19d198151caf
Revises: 20da26039edf
Create Date: 2023-05-19 13:41:45.070918
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision = '19d198151caf'
down_revision = '20da26039edf'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('hero', sa.Column('shoe_size', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('hero', 'shoe_size')
# ### end Alembic commands ###

View file

@ -24,17 +24,18 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
] ]
dependencies = [ dependencies = [
"anyconfig",
"fastapi",
"httpx",
"passlib[bcrypt]",
"polyfactory",
"python-jose[cryptography]",
"python-multipart",
"rich", "rich",
"sqlmodel",
"textual", "textual",
"typer", "typer",
"anyconfig",
"sqlmodel",
"fastapi",
"uvicorn[standard]", "uvicorn[standard]",
"httpx",
"python-jose[cryptography]",
"passlib[bcrypt]",
"python-multipart",
] ]
dynamic = ["version"] dynamic = ["version"]
@ -57,9 +58,9 @@ dependencies = [
"mypy", "mypy",
"pyflyby", "pyflyby",
"pytest", "pytest",
'alembic',
"pytest-cov", "pytest-cov",
"pytest-mock", "pytest-mock",
"pytest-rich",
"ruff", "ruff",
"black", "black",
] ]
@ -96,8 +97,7 @@ exclude_lines = [
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "-ra -q --rich" addopts = "-ra -q"
asyncio_mode = "auto"
testpaths = ["tests"] testpaths = ["tests"]
[tool.coverage_rich] [tool.coverage_rich]

25
tests/test_hero.py Normal file
View file

@ -0,0 +1,25 @@
import tempfile
import pytest
from sqlmodel import Session
from learn_sql_model.config import Config, get_config
from learn_sql_model.factories.hero import HeroFactory
from learn_sql_model.models.hero import Hero
Hero
@pytest.fixture
def config() -> Session:
tmp_db = tempfile.NamedTemporaryFile(suffix=".db")
config = get_config({"database_url": f"sqlite:///{tmp_db.name}"})
config.create_db_and_tables()
return config
def test_post_hero(config: Config) -> None:
hero = HeroFactory().build(name="Batman", age=50)
hero.post(config=config)
assert hero.get(hero.id) == hero
breakpoint()