diff --git a/.gitignore b/.gitignore index e1a3186..9d6f21d 100644 --- a/.gitignore +++ b/.gitignore @@ -962,3 +962,4 @@ FodyWeavers.xsd # 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 +database.db diff --git a/database.db b/database.db index 7934602..8d026be 100644 Binary files a/database.db and b/database.db differ diff --git a/learn_sql_model/api.py b/learn_sql_model/api.py index 4ea7eb1..b110c33 100644 --- a/learn_sql_model/api.py +++ b/learn_sql_model/api.py @@ -1,10 +1,11 @@ from typing import Union from fastapi import FastAPI - import httpx + 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] diff --git a/learn_sql_model/api/hero.py b/learn_sql_model/api/hero.py index 2f05bfb..01fb8bd 100644 --- a/learn_sql_model/api/hero.py +++ b/learn_sql_model/api/hero.py @@ -3,7 +3,7 @@ from typing import Annotated from fastapi import APIRouter, Depends 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() diff --git a/learn_sql_model/cli/api.py b/learn_sql_model/cli/api.py index 1ca18c2..209bbc6 100644 --- a/learn_sql_model/cli/api.py +++ b/learn_sql_model/cli/api.py @@ -2,6 +2,7 @@ import typer import uvicorn from learn_sql_model.cli.common import verbose_callback +from learn_sql_model.config import config api_app = typer.Typer() @@ -25,4 +26,4 @@ def run( 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") diff --git a/learn_sql_model/cli/app.py b/learn_sql_model/cli/app.py index 4973bd0..e2edbd6 100644 --- a/learn_sql_model/cli/app.py +++ b/learn_sql_model/cli/app.py @@ -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.config import config_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 app = typer.Typer( diff --git a/learn_sql_model/cli/hero.py b/learn_sql_model/cli/hero.py index 81aeca9..e15dfd1 100644 --- a/learn_sql_model/cli/hero.py +++ b/learn_sql_model/cli/hero.py @@ -4,7 +4,7 @@ from pydantic_typer import expand_pydantic_args from rich.console import Console import typer -from learn_sql_model.models import Hero +from learn_sql_model.models.hero import Hero hero_app = typer.Typer() diff --git a/learn_sql_model/cli/model.py b/learn_sql_model/cli/model.py new file mode 100644 index 0000000..3803a6e --- /dev/null +++ b/learn_sql_model/cli/model.py @@ -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", + ), +): + ... diff --git a/learn_sql_model/cli/model_app.py b/learn_sql_model/cli/model_app.py deleted file mode 100644 index db43ee4..0000000 --- a/learn_sql_model/cli/model_app.py +++ /dev/null @@ -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() diff --git a/learn_sql_model/config.py b/learn_sql_model/config.py index 172d304..16b37f5 100644 --- a/learn_sql_model/config.py +++ b/learn_sql_model/config.py @@ -3,7 +3,8 @@ from typing import TYPE_CHECKING from pydantic import BaseSettings 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 models = [Hero, Pet] @@ -14,6 +15,7 @@ if TYPE_CHECKING: class Config(BaseSettings): database_url: str = "sqlite:///database.db" + port: int = 5000 class Config: env_prefix = "LEARN_SQL_MODEL_" @@ -35,6 +37,11 @@ class Config(BaseSettings): # app.get("/heroes/")(Hero.read_heroes) -raw_config = load("learn_sql_model") -config = Config(**raw_config) -config.create_db_and_tables() +def get_config(overrides: dict = {}) -> Config: + raw_config = load("learn_sql_model") + config = Config(**raw_config, **overrides) + config.create_db_and_tables() + return config + + +config = get_config() diff --git a/learn_sql_model/factories/__init__.py b/learn_sql_model/factories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learn_sql_model/factories/hero.py b/learn_sql_model/factories/hero.py new file mode 100644 index 0000000..6a98009 --- /dev/null +++ b/learn_sql_model/factories/hero.py @@ -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 diff --git a/learn_sql_model/models.py b/learn_sql_model/models.py deleted file mode 100644 index 9c929ae..0000000 --- a/learn_sql_model/models.py +++ /dev/null @@ -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") diff --git a/learn_sql_model/models/__init__.py b/learn_sql_model/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learn_sql_model/models/fast_model.py b/learn_sql_model/models/fast_model.py new file mode 100644 index 0000000..c5cc569 --- /dev/null +++ b/learn_sql_model/models/fast_model.py @@ -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() diff --git a/learn_sql_model/models/hero.py b/learn_sql_model/models/hero.py new file mode 100644 index 0000000..5b197cf --- /dev/null +++ b/learn_sql_model/models/hero.py @@ -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 diff --git a/learn_sql_model/models/pet.py b/learn_sql_model/models/pet.py new file mode 100644 index 0000000..bc167e3 --- /dev/null +++ b/learn_sql_model/models/pet.py @@ -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") diff --git a/migrations/env.py b/migrations/env.py index ee54604..cd73f61 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -2,10 +2,11 @@ from logging.config import fileConfig from alembic import context from sqlalchemy import engine_from_config, pool - -from learn_sql_model.models import Hero, Pet 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 # access to the values within the .ini file in use. config = context.config diff --git a/migrations/versions/19d198151caf_add_shoe_size.py b/migrations/versions/19d198151caf_add_shoe_size.py new file mode 100644 index 0000000..674b620 --- /dev/null +++ b/migrations/versions/19d198151caf_add_shoe_size.py @@ -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 ### diff --git a/pyproject.toml b/pyproject.toml index 6393bc1..c630615 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,17 +24,18 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ + "anyconfig", + "fastapi", + "httpx", + "passlib[bcrypt]", + "polyfactory", + "python-jose[cryptography]", + "python-multipart", "rich", + "sqlmodel", "textual", "typer", - "anyconfig", - "sqlmodel", - "fastapi", "uvicorn[standard]", - "httpx", - "python-jose[cryptography]", - "passlib[bcrypt]", - "python-multipart", ] dynamic = ["version"] @@ -57,9 +58,9 @@ dependencies = [ "mypy", "pyflyby", "pytest", + 'alembic', "pytest-cov", "pytest-mock", - "pytest-rich", "ruff", "black", ] @@ -96,8 +97,7 @@ exclude_lines = [ ] [tool.pytest.ini_options] -addopts = "-ra -q --rich" -asyncio_mode = "auto" +addopts = "-ra -q" testpaths = ["tests"] [tool.coverage_rich] diff --git a/tests/test_hero.py b/tests/test_hero.py new file mode 100644 index 0000000..40b8be4 --- /dev/null +++ b/tests/test_hero.py @@ -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()