diff --git a/Dockerfile.dev b/Dockerfile.dev index ae11120..dcfba07 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,9 +1,17 @@ FROM python:3.10 ENV DEBIAIN_FRONTEND=noninteractive -ENV PATH="$PATH:/root/.local/bin:/root/.cargo/bin" +ENV PATH="$PATH:/home/smoke/.local/bin:/home/smoke/.cargo/bin" ENV SHELL=zsh -ENV USER=root +ENV USER=smoke +ARG SMOKE_UID=1000 +ARG SMOKE_GID=1000 + +RUN groupadd -f -g ${SMOKE_GID} smoke && \ + useradd -d /home/smoke -s /bin/bash -g ${SMOKE_GID} -u ${SMOKE_UID} smoke +RUN mkdir /home/smoke && chown -R smoke:smoke /home/smoke && mkdir /src && chown smoke:smoke /src +WORKDIR /home/smoke + RUN apt update && \ @@ -16,19 +24,21 @@ RUN apt update && \ stow \ zsh - -WORKDIR /root/downloads +USER smoke +WORKDIR /home/smoke/downloads RUN wget https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage && \ - mkdir -p /root/.local/bin && \ - chmod u+x /root/downloads/nvim.appimage && \ - /root/downloads/nvim.appimage --appimage-extract && \ + mkdir -p /home/smoke/.local/bin && \ + chmod u+x /home/smoke/downloads/nvim.appimage && \ + /home/smoke/downloads/nvim.appimage --appimage-extract && \ rm -rf nvim.appimage && \ ln -s ~/downloads/squashfs-root/usr/bin/nvim ~/.local/bin/nvim && \ cd ~ && \ git clone https://github.com/LazyVim/starter ~/.config/nvim && \ nvim --headless -c 'quitall' +USER root + RUN curl -sS https://starship.rs/install.sh | sh -s -- -y RUN curl -L zellij.dev/launch | sh -s -- help @@ -43,6 +53,8 @@ RUN python3 -m pip install --upgrade pip && \ WORKDIR /app +USER smoke + ## DUPLICATE from Dockerfile ## building FROM learn-sql-model will cause the cache to bust for every ## change, it needs to come after the dev installs. @@ -54,7 +66,7 @@ COPY . . RUN python3 -m hatch env create && \ python3 -m hatch shell -RUN stow bin -t /root/ +RUN stow bin -t /home/smoke/ COPY .env.dev.docker /app/.env.dev diff --git a/learn_sql_model/cli/hero.py b/learn_sql_model/cli/hero.py index 6c993f1..768d389 100644 --- a/learn_sql_model/cli/hero.py +++ b/learn_sql_model/cli/hero.py @@ -8,7 +8,13 @@ import typer from learn_sql_model.config import Config, get_config from learn_sql_model.factories.hero import HeroFactory from learn_sql_model.factories.pet import PetFactory -from learn_sql_model.models.hero import Hero, HeroCreate +from learn_sql_model.models.hero import ( + Hero, + HeroCreate, + HeroDelete, + HeroRead, + HeroUpdate, +) hero_app = typer.Typer() @@ -26,7 +32,7 @@ def get( ) -> Union[Hero, List[Hero]]: "get one hero" config.init() - hero = Hero().get(id=id) + hero = HeroRead.get(id=id, config=config) Console().print(hero) return hero @@ -34,10 +40,13 @@ def get( @hero_app.command() @engorgio(typer=True) def list( + where: Optional[str] = None, config: Config = None, + offset: int = 0, + limit: Optional[int] = None, ) -> Union[Hero, List[Hero]]: "get one hero" - hero = Hero().get() + hero = HeroRead.list(config=config, where=where, offset=offset, limit=limit) Console().print(hero) return hero @@ -49,9 +58,42 @@ def create( config: Config = None, ) -> Hero: "read all the heros" - config.init() + # config.init() hero = hero.post(config=config) Console().print(hero) + return hero + # config.init() + # with Session(config.database.engine) as session: + # db_hero = Hero.from_orm(hero) + # session.add(db_hero) + # session.commit() + # session.refresh(db_hero) + # return db_hero + + +@hero_app.command() +@engorgio(typer=True) +def update( + hero: HeroUpdate, + config: Config = None, +) -> Hero: + "read all the heros" + hero = hero.update(config=config) + Console().print(hero) + return hero + + +@hero_app.command() +@engorgio(typer=True) +def delete( + hero: HeroDelete, + config: Config = None, +) -> Hero: + "read all the heros" + # config.init() + hero = hero.delete(config=config) + return hero + # Console().print(hero) @hero_app.command() diff --git a/learn_sql_model/cli/model.py b/learn_sql_model/cli/model.py index 0fcdc6c..826333d 100644 --- a/learn_sql_model/cli/model.py +++ b/learn_sql_model/cli/model.py @@ -1,6 +1,7 @@ import alembic -from alembic.config import Config import typer +from alembic.config import Config +from copier import run_auto from learn_sql_model.cli.common import verbose_callback @@ -19,13 +20,24 @@ def model( @model_app.command() -def create_revision( +def create( verbose: bool = typer.Option( False, callback=verbose_callback, help="show the log messages", ), - message: str = typer.Option( + template=Path('templates/model') + run_auto(template, Path('.')) + + +@ 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, ), ): @@ -39,23 +51,23 @@ def create_revision( alembic.command.upgrade(config=alembic_cfg, revision="head") -@model_app.command() +@ model_app.command() def checkout( - verbose: bool = typer.Option( + verbose: bool=typer.Option( False, callback=verbose_callback, help="show the log messages", ), - revision: str = typer.Option("head"), + revision: str=typer.Option("head"), ): alembic_cfg = Config("alembic.ini") alembic.command.upgrade(config=alembic_cfg, revision="head") -@model_app.command() +@ model_app.command() def populate( - verbose: bool = typer.Option( + verbose: bool=typer.Option( False, callback=verbose_callback, help="show the log messages", diff --git a/learn_sql_model/models/fast_model.py b/learn_sql_model/models/fast_model.py index eab3e30..1b75c91 100644 --- a/learn_sql_model/models/fast_model.py +++ b/learn_sql_model/models/fast_model.py @@ -43,7 +43,6 @@ class FastModel(SQLModel): with config.database.session as session: if id is None: - print("get all") statement = select(self.__class__) if where is not None: statement = statement.where(where).options() diff --git a/learn_sql_model/models/hero.py b/learn_sql_model/models/hero.py index 4d80df3..2f7c791 100644 --- a/learn_sql_model/models/hero.py +++ b/learn_sql_model/models/hero.py @@ -1,12 +1,14 @@ from typing import Optional -from sqlmodel import Field, Relationship +from fastapi import HTTPException +from pydantic import BaseModel +from sqlmodel import Field, Relationship, SQLModel, Session, select -from learn_sql_model.models.fast_model import FastModel +from learn_sql_model.config import Config from learn_sql_model.models.pet import Pet -class HeroBase(FastModel, table=False): +class HeroBase(SQLModel, table=False): name: str secret_name: str age: Optional[int] = None @@ -23,14 +25,89 @@ class Hero(HeroBase, table=True): class HeroCreate(HeroBase): ... + def post(self, config: Config) -> Hero: + config.init() + with Session(config.database.engine) as session: + db_hero = Hero.from_orm(self) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero + class HeroRead(HeroBase): - id: Optional[int] = Field(default=None, primary_key=True) + id: int + + @classmethod + def get( + cls, + config: Config, + id: int, + ) -> Hero: + + with config.database.session as session: + hero = session.get(Hero, id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + return hero + + @classmethod + def list( + self, + config: Config, + where=None, + offset=0, + limit=None, + ) -> Hero: + + with config.database.session as session: + statement = select(Hero) + if where != "None": + from sqlmodel import text + + statement = statement.where(text(where)) + statement = statement.offset(offset).limit(limit) + heroes = session.exec(statement).all() + return heroes -class HeroUpdate(HeroBase): - ... +class HeroUpdate(SQLModel): + # id is required to get the hero + id: int + + # all other fields, must match the model, but with Optional default None + name: Optional[str] = None + secret_name: Optional[str] = None + age: Optional[int] = None + shoe_size: Optional[int] = None + + pet_id: Optional[int] = Field(default=None, foreign_key="pet.id") + pet: Optional[Pet] = Relationship(back_populates="hero") + + def update(self, config: Config) -> Hero: + with Session(config.database.engine) as session: + db_hero = session.get(Hero, self.id) + if not db_hero: + raise HTTPException(status_code=404, detail="Hero not found") + hero_data = self.dict(exclude_unset=True) + for key, value in hero_data.items(): + if value is not None: + setattr(db_hero, key, value) + session.add(db_hero) + session.commit() + session.refresh(db_hero) + return db_hero -class HeroDelete(HeroBase): - id: Optional[int] = Field(default=None, primary_key=True) +class HeroDelete(BaseModel): + id: int + + def delete(self, config: Config) -> Hero: + config.init() + with Session(config.database.engine) as session: + hero = session.get(Hero, self.id) + if not hero: + raise HTTPException(status_code=404, detail="Hero not found") + session.delete(hero) + session.commit() + return {"ok": True} diff --git a/learn_sql_model/models/pet.py b/learn_sql_model/models/pet.py index 7485657..aae5c0c 100644 --- a/learn_sql_model/models/pet.py +++ b/learn_sql_model/models/pet.py @@ -9,8 +9,27 @@ if TYPE_CHECKING: from learn_sql_model.models.hero import Hero -class Pet(FastModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) +class PetBase(FastModel, table=False): name: str = "Jim" birthday: Optional[datetime] = None hero: "Hero" = Relationship(back_populates="pet") + + +class Pet(PetBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + + +class PetCreate(PetBase): + ... + + +class PetRead(PetBase): + id: int + + +class PetUpdate(PetBase): + ... + + +class PetDelete(PetBase): + id: int diff --git a/templates/model/copier.yml b/templates/model/copier.yml new file mode 100644 index 0000000..f48b700 --- /dev/null +++ b/templates/model/copier.yml @@ -0,0 +1,7 @@ +_min_copier_version: v6.0.0b0 +_exclude: + - README.md + - .git + - copier.yml +name: + type: str