diff --git a/docs/advanced/index.md b/docs/advanced/index.md index f6178249ce..17060bee66 100644 --- a/docs/advanced/index.md +++ b/docs/advanced/index.md @@ -5,6 +5,12 @@ The **Advanced User Guide** is gradually growing, you can already read about som At some point it will include: * How to use `async` and `await` with the async session. +* See: `docs_src\tutorial_async` for working examples as equivalents to `docs_src\tutorial` +* Also see `tests\tutorial_async` for fully functioning pytest tests against the examples +* +* These examples and tests are also passing `mypy` static checking if #PR58 addressing Issue#54 is included +* also note the TODO: for PR#435 subclassing the SQLAlchemey requirements fully into SQLModel +* * How to run migrations. * How to combine **SQLModel** models with SQLAlchemy. * ...and more. 🤓 diff --git a/docs_src/tutorial_async/.python-version b/docs_src/tutorial_async/.python-version new file mode 100644 index 0000000000..55689195cb --- /dev/null +++ b/docs_src/tutorial_async/.python-version @@ -0,0 +1 @@ +3.10.7 diff --git a/docs_src/tutorial_async/__init__.py b/docs_src/tutorial_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/connect_async/__init__.py b/docs_src/tutorial_async/connect_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/connect_async/create_tables_async/__init__.py b/docs_src/tutorial_async/connect_async/create_tables_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/connect_async/create_tables_async/tutorial001_async.py b/docs_src/tutorial_async/connect_async/create_tables_async/tutorial001_async.py new file mode 100644 index 0000000000..f115401afb --- /dev/null +++ b/docs_src/tutorial_async/connect_async/create_tables_async/tutorial001_async.py @@ -0,0 +1,47 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def main() -> None: + await create_db_and_tables() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/delete_async/__init__.py b/docs_src/tutorial_async/connect_async/delete_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/connect_async/delete_async/tutorial001_async.py b/docs_src/tutorial_async/connect_async/delete_async/tutorial001_async.py new file mode 100644 index 0000000000..cb30eed76e --- /dev/null +++ b/docs_src/tutorial_async/connect_async/delete_async/tutorial001_async.py @@ -0,0 +1,94 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team_id = team_preventers.id + session.add(hero_spider_boy) + await session.commit() + await session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + hero_spider_boy.team_id = None + session.add(hero_spider_boy) + await session.commit() + await session.refresh(hero_spider_boy) + print("No longer Preventer:", hero_spider_boy) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/insert_async/__init__.py b/docs_src/tutorial_async/connect_async/insert_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/connect_async/insert_async/tutorial001_async.py b/docs_src/tutorial_async/connect_async/insert_async/tutorial001_async.py new file mode 100644 index 0000000000..8ee48ee283 --- /dev/null +++ b/docs_src/tutorial_async/connect_async/insert_async/tutorial001_async.py @@ -0,0 +1,82 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/select_async/__init__.py b/docs_src/tutorial_async/connect_async/select_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/connect_async/select_async/tutorial001_async.py b/docs_src/tutorial_async/connect_async/select_async/tutorial001_async.py new file mode 100644 index 0000000000..2d4b12a4cc --- /dev/null +++ b/docs_src/tutorial_async/connect_async/select_async/tutorial001_async.py @@ -0,0 +1,91 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: + statement = select(Hero, Team).where(Hero.team_id == Team.id) + results = await session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/select_async/tutorial002_async.py b/docs_src/tutorial_async/connect_async/select_async/tutorial002_async.py new file mode 100644 index 0000000000..c3cbf5a046 --- /dev/null +++ b/docs_src/tutorial_async/connect_async/select_async/tutorial002_async.py @@ -0,0 +1,91 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: + statement = select(Hero, Team).join(Team) + results = await session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/select_async/tutorial003_async.py b/docs_src/tutorial_async/connect_async/select_async/tutorial003_async.py new file mode 100644 index 0000000000..69162abb31 --- /dev/null +++ b/docs_src/tutorial_async/connect_async/select_async/tutorial003_async.py @@ -0,0 +1,91 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: + statement = select(Hero, Team).join(Team, isouter=True) + results = await session.exec(statement) + for hero, team in results: + print("Hero:", hero, "Team:", team) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/select_async/tutorial004_async.py b/docs_src/tutorial_async/connect_async/select_async/tutorial004_async.py new file mode 100644 index 0000000000..c3cb426f26 --- /dev/null +++ b/docs_src/tutorial_async/connect_async/select_async/tutorial004_async.py @@ -0,0 +1,91 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +async def select_heroes() -> None: + async with AsyncSession(engine, expire_on_commit=False) as session: + statement = select(Hero).join(Team).where(Team.name == "Preventers") + results = await session.exec(statement) + for hero in results: + print("Preventer Hero:", hero) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/select_async/tutorial005_async.py b/docs_src/tutorial_async/connect_async/select_async/tutorial005_async.py new file mode 100644 index 0000000000..92401b6b63 --- /dev/null +++ b/docs_src/tutorial_async/connect_async/select_async/tutorial005_async.py @@ -0,0 +1,91 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: + statement = select(Hero, Team).join(Team).where(Team.name == "Preventers") + results = await session.exec(statement) + for hero, team in results: + print("Preventer Hero:", hero, "Team:", team) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/connect_async/update_async/__init__.py b/docs_src/tutorial_async/connect_async/update_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/connect_async/update_async/tutorial001_async.py b/docs_src/tutorial_async/connect_async/update_async/tutorial001_async.py new file mode 100644 index 0000000000..e8a87c3a67 --- /dev/null +++ b/docs_src/tutorial_async/connect_async/update_async/tutorial001_async.py @@ -0,0 +1,88 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Team(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + headquarters: str + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: Annotated[str, Field(index=True)] + secret_name: str + age: Annotated[Optional[int], Field(default_factory=lambda: None, index=True)] + + team_id: Annotated[ + Optional[int], Field(default_factory=lambda: None, foreign_key="team.id") + ] + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + async with AsyncSession(engine, expire_on_commit=False) as session: + team_preventers = Team(name="Preventers", headquarters="Sharp Tower") + team_z_force = Team(name="Z-Force", headquarters="Sister Margaret’s Bar") + session.add(team_preventers) + session.add(team_z_force) + await session.commit() + + hero_deadpond = Hero( + name="Deadpond", secret_name="Dive Wilson", team_id=team_z_force.id + ) + hero_rusty_man = Hero( + name="Rusty-Man", + secret_name="Tommy Sharp", + age=48, + team_id=team_preventers.id, + ) + hero_spider_boy = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + session.add(hero_deadpond) + session.add(hero_rusty_man) + session.add(hero_spider_boy) + await session.commit() + + await session.refresh(hero_deadpond) + await session.refresh(hero_rusty_man) + await session.refresh(hero_spider_boy) + + print("Created hero:", hero_deadpond) + print("Created hero:", hero_rusty_man) + print("Created hero:", hero_spider_boy) + + hero_spider_boy.team_id = team_preventers.id + session.add(hero_spider_boy) + await session.commit() + await session.refresh(hero_spider_boy) + print("Updated hero:", hero_spider_boy) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/select_async/__init__.py b/docs_src/tutorial_async/select_async/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs_src/tutorial_async/select_async/annotations/en/tutorial002.md b/docs_src/tutorial_async/select_async/annotations/en/tutorial002.md new file mode 100644 index 0000000000..2570b542c4 --- /dev/null +++ b/docs_src/tutorial_async/select_async/annotations/en/tutorial002.md @@ -0,0 +1,63 @@ +1. Import from `sqlmodel` everything we will use, including the new `select()` function. + +2. Create the `Hero` class model, representing the `hero` table. + +3. Create the **engine**, we should use a single one shared by all the application code, and that's what we are doing here. + +4. Create all the tables for the models registered in `SQLModel.metadata`. + + This also creates the database if it doesn't exist already. + +5. Create each one of the `Hero` objects. + + You might not have this in your version if you had already created the data in the database. + +6. Create a new **session** and use it to `add` the heroes to the database, and then `commit` the changes. + +7. Create a new **session** to query data. + + !!! tip + Notice that this is a new **session** independent from the one in the other function above. + + But it still uses the same **engine**. We still have one engine for the whole application. + +8. Use the `select()` function to create a statement selecting all the `Hero` objects. + + This selects all the rows in the `hero` table. + +9. Use `session.exec(statement)` to make the **session** use the **engine** to execute the internal SQL statement. + + This will go to the database, execute that SQL, and get the results back. + + It returns a special iterable object that we put in the variable `results`. + + This generates the output: + + ``` + INFO Engine BEGIN (implicit) + INFO Engine SELECT hero.id, hero.name, hero.secret_name, hero.age + FROM hero + INFO Engine [no key 0.00032s] () + ``` + +10. Iterate for each `Hero` object in the `results`. + +11. Print each `hero`. + + The 3 iterations in the `for` loop will generate this output: + + ``` + id=1 name='Deadpond' age=None secret_name='Dive Wilson' + id=2 name='Spider-Boy' age=None secret_name='Pedro Parqueador' + id=3 name='Rusty-Man' age=48 secret_name='Tommy Sharp' + ``` + +12. At this point, after the `with` block, the **session** is closed. + + This generates the output: + + ``` + INFO Engine ROLLBACK + ``` + +13. Add this function `select_heroes()` to the `main()` function so that it is called when we run this program from the command line. diff --git a/docs_src/tutorial_async/select_async/tutorial001_async.py b/docs_src/tutorial_async/select_async/tutorial001_async.py new file mode 100644 index 0000000000..8c25ecbb15 --- /dev/null +++ b/docs_src/tutorial_async/select_async/tutorial001_async.py @@ -0,0 +1,62 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: str + secret_name: str + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + async with AsyncSession(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + await session.commit() + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: + statement = select(Hero) + results = await session.exec(statement) + for hero in results: + print(hero) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/select_async/tutorial002_async.py b/docs_src/tutorial_async/select_async/tutorial002_async.py new file mode 100644 index 0000000000..e3ef3a3f56 --- /dev/null +++ b/docs_src/tutorial_async/select_async/tutorial002_async.py @@ -0,0 +1,62 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession # (1) + + +class Hero(SQLModel, table=True): # (2) + id: Annotated[int, Field(primary_key=True)] + name: str + secret_name: str + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) # (3) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) # (4) + + +async def create_heroes() -> None: + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") # (5) + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + async with AsyncSession(engine) as session: # (6) + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + await session.commit() + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: # (7) + statement = select(Hero) # (8) + results = await session.exec(statement) # (9) + for hero in results: # (10) + print(hero) # (11) + # (12) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() # (13) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/select_async/tutorial003_async.py b/docs_src/tutorial_async/select_async/tutorial003_async.py new file mode 100644 index 0000000000..2ffd07d71a --- /dev/null +++ b/docs_src/tutorial_async/select_async/tutorial003_async.py @@ -0,0 +1,61 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: str + secret_name: str + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + async with AsyncSession(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + await session.commit() + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: + statement = select(Hero) + results = await session.exec(statement) + heroes = results.all() + print(heroes) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs_src/tutorial_async/select_async/tutorial004_async.py b/docs_src/tutorial_async/select_async/tutorial004_async.py new file mode 100644 index 0000000000..74389677f4 --- /dev/null +++ b/docs_src/tutorial_async/select_async/tutorial004_async.py @@ -0,0 +1,62 @@ +import asyncio +from typing import Annotated, Optional + +# TODO change when https://github.com/tiangolo/sqlmodel/pull/435 accepted +# TODO replace following 3 lines with: +# ------ from sqlmodel import AsyncSession, create_async_engine, Field, SQLModel, select +from sqlalchemy.ext.asyncio import create_async_engine +from sqlmodel import Field, SQLModel, select +from sqlmodel.ext.asyncio.session import AsyncSession + + +class Hero(SQLModel, table=True): + id: Annotated[int, Field(primary_key=True)] + name: str + secret_name: str + age: Optional[int] = None + + +sqlite_file_name = "database.db" +sqlite_url = f"sqlite+aiosqlite:///{sqlite_file_name}" + +engine = create_async_engine(sqlite_url, echo=True) + + +async def create_db_and_tables() -> None: + meta = SQLModel.metadata + + async with engine.begin() as conn: + await conn.run_sync(meta.drop_all) + await conn.run_sync(meta.create_all) + + +async def create_heroes() -> None: + hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") + hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") + hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) + + async with AsyncSession(engine) as session: + session.add(hero_1) + session.add(hero_2) + session.add(hero_3) + + await session.commit() + + +async def select_heroes() -> None: + async with AsyncSession(engine) as session: + # TODO: in async, this does not work `await session.exec(select(Hero)).all()` + # heroes = await session.exec(select(Hero)).all() + results = await session.exec(select(Hero)) + heroes = results.all() + print(heroes) + + +async def main() -> None: + await create_db_and_tables() + await create_heroes() + await select_heroes() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/pyproject.toml b/pyproject.toml index 7f5e7f8037..ed9bbad9bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,16 +30,18 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.6.1" +python = "^3.7" SQLAlchemy = ">=1.4.17,<=1.4.41" pydantic = "^1.8.2" sqlalchemy2-stubs = {version = "*", allow-prereleases = true} +aiosqlite = "^0.17.0" [tool.poetry.dev-dependencies] pytest = "^6.2.4" mypy = "0.930" -flake8 = "^3.9.2" -black = {version = "^21.5-beta.1", python = "^3.7"} +flake8 = "^3.8" +importlib-metadata = "4.13.0" #required for flake8 with 3.7 see https://github.com/PyCQA/flake8/issues/1701 +black = "^22.10.0" mkdocs = "^1.2.1" mkdocs-material = "^8.1.4" mdx-include = "^1.4.1" @@ -50,6 +52,9 @@ autoflake = "^1.4" isort = "^5.9.3" async_generator = {version = "*", python = "~3.6"} async-exit-stack = {version = "*", python = "~3.6"} +pytest-asyncio = "^0.19.0" +SQLAlchemy = {extras = ["asyncio"], version = "^1.4.41"} + [build-system] requires = ["poetry-core"] diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d343c698e9..ba485e65e1 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -11,6 +11,7 @@ Callable, ClassVar, Dict, + ForwardRef, List, Mapping, Optional, @@ -29,7 +30,7 @@ from pydantic.fields import FieldInfo as PydanticFieldInfo from pydantic.fields import ModelField, Undefined, UndefinedType from pydantic.main import ModelMetaclass, validate_model -from pydantic.typing import ForwardRef, NoArgAnyCallable, resolve_annotations +from pydantic.typing import NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation from sqlalchemy import Boolean, Column, Date, DateTime from sqlalchemy import Enum as sa_Enum diff --git a/tests/test_async_tutorial/test_async_connect/__init__.py b/tests/test_async_tutorial/test_async_connect/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_async_tutorial/test_async_connect/test_async_create_connected_tables/__init__.py b/tests/test_async_tutorial/test_async_connect/test_async_create_connected_tables/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_async_tutorial/test_async_connect/test_async_create_connected_tables/test_async_tutorial001.py b/tests/test_async_tutorial/test_async_connect/test_async_create_connected_tables/test_async_tutorial001.py new file mode 100644 index 0000000000..c36285a917 --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_create_connected_tables/test_async_tutorial001.py @@ -0,0 +1,43 @@ +import sys + +import pytest +from sqlalchemy import inspect +from sqlalchemy.ext.asyncio import create_async_engine + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio +async def test_async_tutorial001() -> None: + from docs_src.tutorial_async.connect_async.create_tables_async import ( + tutorial001_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + + await mod.main() + + # Following code lines are for sync implementation , see following note regarding async + # https://docs.sqlalchemy.org/en/14/errors.html#error-xd3s + # insp: Inspector = inspect(mod.engine) + # assert insp.has_table(str(mod.Hero.__tablename__)) + # assert insp.has_table(str(mod.Team.__tablename__)) + + # following is recommended workaround + async with mod.engine.connect() as conn: + tables = await conn.run_sync( + lambda sync_conn: inspect(sync_conn).get_table_names() + ) + assert str(mod.Team.__tablename__) in tables + assert str(mod.Hero.__tablename__) in tables + + # TODO; work out how to call insp.has_tables with run_sync + # async with mod.engine.connect() as conn: + # insp: Inspector = await conn.run_sync( + # lambda sync_conn: inspect(sync_conn) + # ) + # assert insp.has_table(str(mod.Hero.__tablename__)) + # assert insp.has_table(str(mod.Team.__tablename__)) diff --git a/tests/test_async_tutorial/test_async_connect/test_async_delete/__init__.py b/tests/test_async_tutorial/test_async_connect/test_async_delete/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_async_tutorial/test_async_connect/test_async_delete/test_async_tutorial001.py b/tests/test_async_tutorial/test_async_connect/test_async_delete/test_async_tutorial001.py new file mode 100644 index 0000000000..796b33024b --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_delete/test_async_tutorial001.py @@ -0,0 +1,83 @@ +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 1, + "name": "Spider-Boy", + }, + ], + [ + "No longer Preventer:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], +] + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_tutorial_async(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.delete_async import ( + tutorial001_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls diff --git a/tests/test_async_tutorial/test_async_connect/test_async_insert/__init__.py b/tests/test_async_tutorial/test_async_connect/test_async_insert/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_async_tutorial/test_async_connect/test_async_insert/test_async_tutorial001.py b/tests/test_async_tutorial/test_async_connect/test_async_insert/test_async_tutorial001.py new file mode 100644 index 0000000000..aefdc7a99f --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_insert/test_async_tutorial001.py @@ -0,0 +1,63 @@ +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], +] + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_async_tutorial001(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.insert_async import ( + tutorial001_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls diff --git a/tests/test_async_tutorial/test_async_connect/test_async_select/__init__.py b/tests/test_async_tutorial/test_async_connect/test_async_select/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial001_tutorial002.py b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial001_tutorial002.py new file mode 100644 index 0000000000..7d2cb667e7 --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial001_tutorial002.py @@ -0,0 +1,105 @@ +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + "Team:", + {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + ], + [ + "Hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + "Team:", + {"id": 1, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], +] + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_tutorial001(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.select_async import ( + tutorial001_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_tutorial002(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.select_async import ( + tutorial002_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls diff --git a/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial003.py b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial003.py new file mode 100644 index 0000000000..f2d7b8e2e8 --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial003.py @@ -0,0 +1,99 @@ +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + "Team:", + {"id": 2, "name": "Z-Force", "headquarters": "Sister Margaret’s Bar"}, + ], + [ + "Hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + "Team:", + {"id": 1, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], + [ + "Hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + "Team:", + None, + ], +] + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_tutorial(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.select_async import ( + tutorial003_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls diff --git a/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial004.py b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial004.py new file mode 100644 index 0000000000..2131137579 --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial004.py @@ -0,0 +1,73 @@ +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Preventer Hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], +] + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_tutorial(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.select_async import ( + tutorial004_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls diff --git a/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial005.py b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial005.py new file mode 100644 index 0000000000..468d0532a3 --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_select/test_async_tutorial005.py @@ -0,0 +1,75 @@ +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Preventer Hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + "Team:", + {"id": 1, "name": "Preventers", "headquarters": "Sharp Tower"}, + ], +] + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_tutorial(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.select_async import ( + tutorial005_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls diff --git a/tests/test_async_tutorial/test_async_connect/test_async_update/__init__.py b/tests/test_async_tutorial/test_async_connect/test_async_update/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_async_tutorial/test_async_connect/test_async_update/test_async_tutorial001.py b/tests/test_async_tutorial/test_async_connect/test_async_update/test_async_tutorial001.py new file mode 100644 index 0000000000..b62072ab93 --- /dev/null +++ b/tests/test_async_tutorial/test_async_connect/test_async_update/test_async_tutorial001.py @@ -0,0 +1,73 @@ +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + +expected_calls = [ + [ + "Created hero:", + { + "age": None, + "id": 1, + "secret_name": "Dive Wilson", + "team_id": 2, + "name": "Deadpond", + }, + ], + [ + "Created hero:", + { + "age": 48, + "id": 2, + "secret_name": "Tommy Sharp", + "team_id": 1, + "name": "Rusty-Man", + }, + ], + [ + "Created hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": None, + "name": "Spider-Boy", + }, + ], + [ + "Updated hero:", + { + "age": None, + "id": 3, + "secret_name": "Pedro Parqueador", + "team_id": 1, + "name": "Spider-Boy", + }, + ], +] + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio() +async def test_tutorial(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.connect_async.update_async import ( + tutorial001_async as mod, + ) + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + assert calls == expected_calls diff --git a/tests/test_async_tutorial/test_async_select/__init__.py b/tests/test_async_tutorial/test_async_select/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_async_tutorial/test_async_select/test_async_tutorial001_tutorial002.py b/tests/test_async_tutorial/test_async_select/test_async_tutorial001_tutorial002.py new file mode 100644 index 0000000000..4ec7fe464e --- /dev/null +++ b/tests/test_async_tutorial/test_async_select/test_async_tutorial001_tutorial002.py @@ -0,0 +1,65 @@ +import sys +from typing import Any, Dict, List, Union +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + + +def check_calls(calls: List[List[Union[str, Dict[str, Any]]]]) -> None: + assert calls[0][0] == { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": 1, + } + assert calls[1][0] == { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": 2, + } + assert calls[2][0] == { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + "id": 3, + } + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio +async def test_tutorial_async_001(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.select_async import tutorial001_async as mod + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + check_calls(calls) + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio +async def test_tutorial_async_002(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.select_async import tutorial002_async as mod + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + check_calls(calls) diff --git a/tests/test_async_tutorial/test_async_select/test_async_tutorial003_tutorial004.py b/tests/test_async_tutorial/test_async_select/test_async_tutorial003_tutorial004.py new file mode 100644 index 0000000000..88d3c79dfe --- /dev/null +++ b/tests/test_async_tutorial/test_async_select/test_async_tutorial003_tutorial004.py @@ -0,0 +1,70 @@ +import sys +from typing import Any, Dict, List, Union +from unittest.mock import patch + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +from tests.conftest import get_testing_print_function + + +def check_calls(calls: List[List[List[Union[str, Dict[str, Any]]]]]) -> None: + expected_result: List[Union[str, Dict[str, Any]]] = [ + { + "name": "Deadpond", + "secret_name": "Dive Wilson", + "age": None, + "id": 1, + }, + { + "name": "Spider-Boy", + "secret_name": "Pedro Parqueador", + "age": None, + "id": 2, + }, + { + "name": "Rusty-Man", + "secret_name": "Tommy Sharp", + "age": 48, + "id": 3, + }, + ] + + assert calls[0][0] == expected_result + + +beforePYTHON3_9 = sys.version_info < (3, 9) +reasonPEP593 = "Annotations(PEP593 https://peps.python.org/pep-0593/) only compatible with Python ver >= 3.9" + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio +async def test_tutorial_003(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.select_async import tutorial003_async as mod + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + check_calls(calls) + + +@pytest.mark.skipif(beforePYTHON3_9, reason=reasonPEP593) +@pytest.mark.asyncio +async def test_tutorial_004(clear_sqlmodel: Any) -> None: + from docs_src.tutorial_async.select_async import tutorial004_async as mod + + mod.sqlite_url = "sqlite+aiosqlite://" + mod.engine = create_async_engine(mod.sqlite_url) + calls: List[Any] = [] + + new_print = get_testing_print_function(calls) + + with patch("builtins.print", new=new_print): + await mod.main() + check_calls(calls)